diff --git a/app/Api/V1/Controllers/Insight/Expense/AccountController.php b/app/Api/V1/Controllers/Insight/Expense/AccountController.php index 83d532c12e..b41f203da6 100644 --- a/app/Api/V1/Controllers/Insight/Expense/AccountController.php +++ b/app/Api/V1/Controllers/Insight/Expense/AccountController.php @@ -25,7 +25,7 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Controllers\Insight\Expense; use FireflyIII\Api\V1\Controllers\Controller; -use FireflyIII\Api\V1\Requests\Insight\ExpenseRequest; +use FireflyIII\Api\V1\Requests\Insight\GenericRequest; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\OperationsRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; @@ -33,25 +33,11 @@ use FireflyIII\Support\Http\Api\ApiSupport; use Illuminate\Http\JsonResponse; /** - * TODO per object group? - * TODO transfers voor piggies? - * TODO currency? - * TODO net worth? * * Class AccountController * * Shows expense information grouped or limited by date. * Ie. all expenses grouped by account + currency. - * - * /api/v1/insight/expenses/budget - * Expenses per budget or no budget. Can be limited by date and by asset account. - * /api/v1/insight/expenses/budget - * Also per budget limit. - * /api/v1/insight/expenses/category - * Expenses per category or no category. Can be limited by date and by asset account. - * /api/v1/insight/expenses/bill - * Expenses per bill or no bill. Can be limited by date and by asset account. - * */ class AccountController extends Controller { @@ -87,17 +73,18 @@ class AccountController extends Controller } /** - * @param ExpenseRequest $request + * @param GenericRequest $request * * @return JsonResponse */ - public function expense(ExpenseRequest $request): JsonResponse + public function expense(GenericRequest $request): JsonResponse { $start = $request->getStart(); $end = $request->getEnd(); $assetAccounts = $request->getAssetAccounts(); $expenseAccounts = $request->getExpenseAccounts(); $expenses = $this->opsRepository->sumExpenses($start, $end, $assetAccounts, $expenseAccounts); + $result = []; /** @var array $expense */ foreach ($expenses as $expense) { @@ -113,16 +100,17 @@ class AccountController extends Controller } /** - * @param ExpenseRequest $request + * @param GenericRequest $request * * @return JsonResponse */ - public function asset(ExpenseRequest $request): JsonResponse + public function asset(GenericRequest $request): JsonResponse { $start = $request->getStart(); $end = $request->getEnd(); $assetAccounts = $request->getAssetAccounts(); $expenses = $this->opsRepository->sumExpenses($start, $end, $assetAccounts); + $result = []; /** @var array $expense */ foreach ($expenses as $expense) { diff --git a/app/Api/V1/Controllers/Insight/Expense/BillController.php b/app/Api/V1/Controllers/Insight/Expense/BillController.php new file mode 100644 index 0000000000..8cc53cc329 --- /dev/null +++ b/app/Api/V1/Controllers/Insight/Expense/BillController.php @@ -0,0 +1,166 @@ +. + */ + +namespace FireflyIII\Api\V1\Controllers\Insight\Expense; + + +use FireflyIII\Api\V1\Controllers\Controller; +use FireflyIII\Api\V1\Requests\Insight\GenericRequest; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; +use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Bill\BillRepositoryInterface; +use Illuminate\Http\JsonResponse; + +/** + * Class BillController + */ +class BillController extends Controller +{ + private BillRepositoryInterface $repository; + + /** + * BillController constructor. + */ + public function __construct() + { + parent::__construct(); + $this->middleware( + function ($request, $next) { + $user = auth()->user(); + $this->repository = app(BillRepositoryInterface::class); + $this->repository->setUser($user); + + return $next($request); + } + ); + } + + /** + * Expenses per bill, possibly filtered by bill and account. + * + * @param GenericRequest $request + * + * @return JsonResponse + */ + public function bill(GenericRequest $request): JsonResponse + { + $accounts = $request->getAssetAccounts(); + $bills = $request->getBills(); + $start = $request->getStart(); + $end = $request->getEnd(); + $response = []; + + // get all bills: + if (0 === $bills->count()) { + $bills = $this->repository->getBills(); + } + + // collect all expenses in this period (regardless of type) by the given bills and accounts. + $collector = app(GroupCollectorInterface::class); + $collector->setTypes([TransactionType::WITHDRAWAL])->setRange($start, $end)->setSourceAccounts($accounts); + $collector->setBills($bills); + + $genericSet = $collector->getExtractedJournals(); + foreach ($genericSet as $journal) { + $billId = (int)$journal['bill_id']; + $currencyId = (int)$journal['currency_id']; + $foreignCurrencyId = (int)$journal['foreign_currency_id']; + $key = sprintf('%d-%d', $billId, $currencyId); + $foreignKey = sprintf('%d-%d', $billId, $foreignCurrencyId); + + if (0 !== $currencyId) { + $response[$key] = $response[$key] ?? [ + 'id' => (string)$billId, + 'name' => $journal['bill_name'], + 'difference' => '0', + 'difference_float' => 0, + 'currency_id' => (string)$currencyId, + 'currency_code' => $journal['currency_code'], + ]; + $response[$key]['difference'] = bcadd($response[$key]['difference'], $journal['amount']); + $response[$key]['difference_float'] = (float)$response[$key]['difference']; + } + if (0 !== $foreignCurrencyId) { + $response[$foreignKey] = $response[$foreignKey] ?? [ + 'difference' => '0', + 'difference_float' => 0, + 'currency_id' => (string)$foreignCurrencyId, + 'currency_code' => $journal['foreign_currency_code'], + ]; + $response[$foreignKey]['difference'] = bcadd($response[$foreignKey]['difference'], $journal['foreign_amount']); + $response[$foreignKey]['difference_float'] = (float)$response[$foreignKey]['difference']; + } + } + + return response()->json(array_values($response)); + } + + /** + * Expenses for no bill filtered by account. + * + * @param GenericRequest $request + * + * @return JsonResponse + */ + public function noBill(GenericRequest $request): JsonResponse + { + $accounts = $request->getAssetAccounts(); + $start = $request->getStart(); + $end = $request->getEnd(); + $response = []; + + // collect all expenses in this period (regardless of type) by the given bills and accounts. + $collector = app(GroupCollectorInterface::class); + $collector->setTypes([TransactionType::WITHDRAWAL])->setRange($start, $end)->setSourceAccounts($accounts); + $collector->withoutBill(); + + $genericSet = $collector->getExtractedJournals(); + + foreach ($genericSet as $journal) { + $currencyId = (int)$journal['currency_id']; + $foreignCurrencyId = (int)$journal['foreign_currency_id']; + + if (0 !== $currencyId) { + $response[$currencyId] = $response[$currencyId] ?? [ + 'difference' => '0', + 'difference_float' => 0, + 'currency_id' => (string)$currencyId, + 'currency_code' => $journal['currency_code'], + ]; + $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], $journal['amount']); + $response[$currencyId]['difference_float'] = (float)$response[$currencyId]['difference']; + } + if (0 !== $foreignCurrencyId) { + $response[$foreignCurrencyId] = $response[$foreignCurrencyId] ?? [ + 'difference' => '0', + 'difference_float' => 0, + 'currency_id' => (string)$foreignCurrencyId, + 'currency_code' => $journal['foreign_currency_code'], + ]; + $response[$foreignCurrencyId]['difference'] = bcadd($response[$foreignCurrencyId]['difference'], $journal['foreign_amount']); + $response[$foreignCurrencyId]['difference_float'] = (float)$response[$foreignCurrencyId]['difference']; + } + } + + return response()->json(array_values($response)); + } + +} \ No newline at end of file diff --git a/app/Api/V1/Controllers/Insight/Expense/BudgetController.php b/app/Api/V1/Controllers/Insight/Expense/BudgetController.php index cc8fd850ce..69deaa6e84 100644 --- a/app/Api/V1/Controllers/Insight/Expense/BudgetController.php +++ b/app/Api/V1/Controllers/Insight/Expense/BudgetController.php @@ -23,7 +23,7 @@ namespace FireflyIII\Api\V1\Controllers\Insight\Expense; use FireflyIII\Api\V1\Controllers\Controller; -use FireflyIII\Api\V1\Requests\Insight\ExpenseRequest; +use FireflyIII\Api\V1\Requests\Insight\GenericRequest; use FireflyIII\Models\Budget; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Budget\NoBudgetRepositoryInterface; @@ -64,11 +64,11 @@ class BudgetController extends Controller } /** - * @param ExpenseRequest $request + * @param GenericRequest $request * * @return JsonResponse */ - public function budget(ExpenseRequest $request): JsonResponse + public function budget(GenericRequest $request): JsonResponse { $start = $request->getStart(); $end = $request->getEnd(); @@ -98,11 +98,11 @@ class BudgetController extends Controller } /** - * @param ExpenseRequest $request + * @param GenericRequest $request * * @return JsonResponse */ - public function noBudget(ExpenseRequest $request): JsonResponse + public function noBudget(GenericRequest $request): JsonResponse { $start = $request->getStart(); $end = $request->getEnd(); diff --git a/app/Api/V1/Controllers/Insight/Expense/CategoryController.php b/app/Api/V1/Controllers/Insight/Expense/CategoryController.php index 1b654f66cd..7d2b5de843 100644 --- a/app/Api/V1/Controllers/Insight/Expense/CategoryController.php +++ b/app/Api/V1/Controllers/Insight/Expense/CategoryController.php @@ -5,7 +5,7 @@ namespace FireflyIII\Api\V1\Controllers\Insight\Expense; use FireflyIII\Api\V1\Controllers\Controller; -use FireflyIII\Api\V1\Requests\Insight\ExpenseRequest; +use FireflyIII\Api\V1\Requests\Insight\GenericRequest; use FireflyIII\Models\Category; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\Repositories\Category\NoCategoryRepositoryInterface; @@ -46,11 +46,11 @@ class CategoryController extends Controller } /** - * @param ExpenseRequest $request + * @param GenericRequest $request * * @return JsonResponse */ - public function category(ExpenseRequest $request): JsonResponse + public function category(GenericRequest $request): JsonResponse { $start = $request->getStart(); $end = $request->getEnd(); @@ -80,11 +80,11 @@ class CategoryController extends Controller } /** - * @param ExpenseRequest $request + * @param GenericRequest $request * * @return JsonResponse */ - public function noCategory(ExpenseRequest $request): JsonResponse + public function noCategory(GenericRequest $request): JsonResponse { $start = $request->getStart(); $end = $request->getEnd(); diff --git a/app/Api/V1/Controllers/Insight/Expense/PeriodController.php b/app/Api/V1/Controllers/Insight/Expense/PeriodController.php index 6dc909cf59..0fd6da9c3a 100644 --- a/app/Api/V1/Controllers/Insight/Expense/PeriodController.php +++ b/app/Api/V1/Controllers/Insight/Expense/PeriodController.php @@ -23,7 +23,7 @@ namespace FireflyIII\Api\V1\Controllers\Insight\Expense; use FireflyIII\Api\V1\Controllers\Controller; -use FireflyIII\Api\V1\Requests\Insight\ExpenseRequest; +use FireflyIII\Api\V1\Requests\Insight\GenericRequest; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Models\TransactionType; use Illuminate\Http\JsonResponse; @@ -35,11 +35,11 @@ class PeriodController extends Controller { /** - * @param ExpenseRequest $request + * @param GenericRequest $request * * @return JsonResponse */ - public function total(ExpenseRequest $request): JsonResponse + public function total(GenericRequest $request): JsonResponse { $accounts = $request->getAssetAccounts(); $start = $request->getStart(); diff --git a/app/Api/V1/Controllers/Insight/Expense/TagController.php b/app/Api/V1/Controllers/Insight/Expense/TagController.php new file mode 100644 index 0000000000..98a84457e4 --- /dev/null +++ b/app/Api/V1/Controllers/Insight/Expense/TagController.php @@ -0,0 +1,173 @@ +. + */ + +namespace FireflyIII\Api\V1\Controllers\Insight\Expense; + + +use FireflyIII\Api\V1\Controllers\Controller; +use FireflyIII\Api\V1\Requests\Insight\GenericRequest; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; +use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Tag\TagRepositoryInterface; +use Illuminate\Http\JsonResponse; + +/** + * Class TagController + */ +class TagController extends Controller +{ + private TagRepositoryInterface $repository; + + /** + * TagController constructor. + */ + public function __construct() + { + parent::__construct(); + $this->middleware( + function ($request, $next) { + $user = auth()->user(); + $this->repository = app(TagRepositoryInterface::class); + $this->repository->setUser($user); + + return $next($request); + } + ); + } + + /** + * Expenses per tag, possibly filtered by tag and account. + * + * @param GenericRequest $request + * + * @return JsonResponse + */ + public function tag(GenericRequest $request): JsonResponse + { + $accounts = $request->getAssetAccounts(); + $tags = $request->getTags(); + $start = $request->getStart(); + $end = $request->getEnd(); + $response = []; + + // get all tags: + if (0 === $tags->count()) { + $tags = $this->repository->get(); + } + + // collect all expenses in this period (regardless of type) by the given bills and accounts. + $collector = app(GroupCollectorInterface::class); + $collector->setTypes([TransactionType::WITHDRAWAL])->setRange($start, $end)->setSourceAccounts($accounts); + $collector->setTags($tags); + $genericSet = $collector->getExtractedJournals(); + /** @var array $entry */ + foreach ($genericSet as $journal) { + $currencyId = (int)$journal['currency_id']; + $foreignCurrencyId = (int)$journal['foreign_currency_id']; + + /** @var array $tag */ + foreach ($journal['tags'] as $tag) { + $tagId = $tag['id']; + $key = sprintf('%d-%d', $tagId, $currencyId); + $foreignKey = sprintf('%d-%d', $tagId, $foreignCurrencyId); + + // on currency ID + if (0 !== $currencyId) { + $response[$key] = $response[$key] ?? [ + 'id' => (string)$tagId, + 'name' => $tag['name'], + 'difference' => '0', + 'difference_float' => 0, + 'currency_id' => (string)$currencyId, + 'currency_code' => $journal['currency_code'], + ]; + $response[$key]['difference'] = bcadd($response[$key]['difference'], $journal['amount']); + $response[$key]['difference_float'] = (float)$response[$key]['difference']; + } + + // on foreign ID + if (0 !== $foreignCurrencyId) { + $response[$foreignKey] = $journal[$foreignKey] ?? [ + 'difference' => '0', + 'difference_float' => 0, + 'currency_id' => (string)$foreignCurrencyId, + 'currency_code' => $journal['foreign_currency_code'], + ]; + $response[$foreignKey]['difference'] = bcadd($response[$foreignKey]['difference'], $journal['foreign_amount']); + $response[$foreignKey]['difference_float'] = (float)$response[$foreignKey]['difference']; + } + } + } + + return response()->json(array_values($response)); + } + + /** + * Expenses for no tag filtered by account. + * + * @param GenericRequest $request + * + * @return JsonResponse + */ + public function noTag(GenericRequest $request): JsonResponse + { + $accounts = $request->getAssetAccounts(); + $start = $request->getStart(); + $end = $request->getEnd(); + $response = []; + + // collect all expenses in this period (regardless of type) by the given bills and accounts. + $collector = app(GroupCollectorInterface::class); + $collector->setTypes([TransactionType::WITHDRAWAL])->setRange($start, $end)->setSourceAccounts($accounts); + $collector->withoutTags(); + + $genericSet = $collector->getExtractedJournals(); + + foreach ($genericSet as $journal) { + $currencyId = (int)$journal['currency_id']; + $foreignCurrencyId = (int)$journal['foreign_currency_id']; + + if (0 !== $currencyId) { + $response[$currencyId] = $response[$currencyId] ?? [ + 'difference' => '0', + 'difference_float' => 0, + 'currency_id' => (string)$currencyId, + 'currency_code' => $journal['currency_code'], + ]; + $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], $journal['amount']); + $response[$currencyId]['difference_float'] = (float)$response[$currencyId]['difference']; + } + if (0 !== $foreignCurrencyId) { + $response[$foreignCurrencyId] = $response[$foreignCurrencyId] ?? [ + 'difference' => '0', + 'difference_float' => 0, + 'currency_id' => (string)$foreignCurrencyId, + 'currency_code' => $journal['foreign_currency_code'], + ]; + $response[$foreignCurrencyId]['difference'] = bcadd($response[$foreignCurrencyId]['difference'], $journal['foreign_amount']); + $response[$foreignCurrencyId]['difference_float'] = (float)$response[$foreignCurrencyId]['difference']; + } + } + + return response()->json(array_values($response)); + } + +} \ No newline at end of file diff --git a/app/Api/V1/Controllers/Insight/Income/AccountController.php b/app/Api/V1/Controllers/Insight/Income/AccountController.php index fd401baecf..8fc49cee7c 100644 --- a/app/Api/V1/Controllers/Insight/Income/AccountController.php +++ b/app/Api/V1/Controllers/Insight/Income/AccountController.php @@ -24,26 +24,29 @@ declare(strict_types=1); namespace FireflyIII\Api\V1\Controllers\Insight\Income; -use Carbon\Carbon; use FireflyIII\Api\V1\Controllers\Controller; -use FireflyIII\Api\V1\Requests\DateRequest; -use FireflyIII\Models\AccountType; +use FireflyIII\Api\V1\Requests\Insight\GenericRequest; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Account\OperationsRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Support\Http\Api\ApiSupport; -use FireflyIII\User; use Illuminate\Http\JsonResponse; /** + * * Class AccountController + * + * Shows income information grouped or limited by date. + * Ie. all income grouped by account + currency. + * TODO same code as Expense/AccountController. */ class AccountController extends Controller { use ApiSupport; - private CurrencyRepositoryInterface $currencyRepository; - private AccountRepositoryInterface $repository; - + private CurrencyRepositoryInterface $currencyRepository; + private AccountRepositoryInterface $repository; + private OperationsRepositoryInterface $opsRepository; /** * AccountController constructor. @@ -55,7 +58,6 @@ class AccountController extends Controller parent::__construct(); $this->middleware( function ($request, $next) { - /** @var User $user */ $user = auth()->user(); $this->repository = app(AccountRepositoryInterface::class); $this->repository->setUser($user); @@ -63,70 +65,67 @@ class AccountController extends Controller $this->currencyRepository = app(CurrencyRepositoryInterface::class); $this->currencyRepository->setUser($user); + $this->opsRepository = app(OperationsRepositoryInterface::class); + $this->opsRepository->setUser($user); + return $next($request); } ); } /** - * @param DateRequest $request + * // TOOD same as + * @param GenericRequest $request * * @return JsonResponse */ - public function revenue(DateRequest $request): JsonResponse + public function revenue(GenericRequest $request): JsonResponse { - // parameters for chart: - $dates = $request->getAll(); - /** @var Carbon $start */ - $start = $dates['start']; - /** @var Carbon $end */ - $end = $dates['end']; + $start = $request->getStart(); + $end = $request->getEnd(); + $assetAccounts = $request->getAssetAccounts(); + $revenueAccounts = $request->getRevenueAccounts(); + $income = $this->opsRepository->sumIncome($start, $end, $assetAccounts, $revenueAccounts); + $result = []; - $start->subDay(); - - // prep some vars: - $currencies = []; - $chartData = []; - $tempData = []; - - // grab all accounts and names - $accounts = $this->repository->getAccountsByType([AccountType::REVENUE]); - $accountNames = $this->extractNames($accounts); - $startBalances = app('steam')->balancesPerCurrencyByAccounts($accounts, $start); - $endBalances = app('steam')->balancesPerCurrencyByAccounts($accounts, $end); - - // loop the end balances. This is an array for each account ($expenses) - foreach ($endBalances as $accountId => $expenses) { - $accountId = (int)$accountId; - // loop each expense entry (each entry can be a different currency). - foreach ($expenses as $currencyId => $endAmount) { - $currencyId = (int)$currencyId; - - // see if there is an accompanying start amount. - // grab the difference and find the currency. - $startAmount = $startBalances[$accountId][$currencyId] ?? '0'; - $diff = bcsub($endAmount, $startAmount); - $currencies[$currencyId] = $currencies[$currencyId] ?? $this->currencyRepository->findNull($currencyId); - if (0 !== bccomp($diff, '0')) { - // store the values in a temporary array. - $tempData[] = [ - 'id' => $accountId, - 'name' => $accountNames[$accountId], - 'difference' => $diff, - 'difference_float' => (float)$diff, - 'currency_id' => $currencyId, - 'currency_code' => $currencies[$currencyId]->code, - ]; - } - } + /** @var array $entry */ + foreach ($income as $entry) { + $result[] = [ + 'difference' => $entry['sum'], + 'difference_float' => (float)$entry['sum'], + 'currency_id' => (string)$entry['currency_id'], + 'currency_code' => $entry['currency_code'], + ]; } + return response()->json($result); + } - // sort temp array by amount. - $amounts = array_column($tempData, 'difference_float'); - array_multisort($amounts, SORT_ASC, $tempData); + /** + * TODO same code as Expense/AccountController. + * + * @param GenericRequest $request + * + * @return JsonResponse + */ + public function asset(GenericRequest $request): JsonResponse + { + $start = $request->getStart(); + $end = $request->getEnd(); + $assetAccounts = $request->getAssetAccounts(); + $income = $this->opsRepository->sumIncome($start, $end, $assetAccounts); + $result = []; + /** @var array $entry */ + foreach ($income as $entry) { + $result[] = [ + 'difference' => $entry['sum'], + 'difference_float' => (float)$entry['sum'], + 'currency_id' => (string)$entry['currency_id'], + 'currency_code' => $entry['currency_code'], + ]; + } - return response()->json($tempData); + return response()->json($result); } } diff --git a/app/Api/V1/Controllers/Insight/Income/CategoryController.php b/app/Api/V1/Controllers/Insight/Income/CategoryController.php new file mode 100644 index 0000000000..c368bd5b9a --- /dev/null +++ b/app/Api/V1/Controllers/Insight/Income/CategoryController.php @@ -0,0 +1,108 @@ +middleware( + function ($request, $next) { + $this->opsRepository = app(OperationsRepositoryInterface::class); + $this->repository = app(CategoryRepositoryInterface::class); + $this->noRepository = app(NoCategoryRepositoryInterface::class); + $user = auth()->user(); + $this->opsRepository->setUser($user); + $this->repository->setUser($user); + $this->noRepository->setUser($user); + + return $next($request); + } + ); + } + + /** + * @param GenericRequest $request + * + * @return JsonResponse + */ + public function category(GenericRequest $request): JsonResponse + { + $start = $request->getStart(); + $end = $request->getEnd(); + $categories = $request->getCategories(); + $assetAccounts = $request->getAssetAccounts(); + $result = []; + if (0 === $categories->count()) { + $categories = $this->repository->getCategories(); + } + /** @var Category $category */ + foreach ($categories as $category) { + $expenses = $this->opsRepository->sumIncome($start, $end, $assetAccounts, new Collection([$category])); + /** @var array $expense */ + foreach ($expenses as $expense) { + $result[] = [ + 'id' => (string)$category->id, + 'name' => $category->name, + 'difference' => $expense['sum'], + 'difference_float' => (float)$expense['sum'], + 'currency_id' => (string)$expense['currency_id'], + 'currency_code' => $expense['currency_code'], + ]; + } + } + + return response()->json($result); + } + + /** + * @param GenericRequest $request + * + * @return JsonResponse + */ + public function noCategory(GenericRequest $request): JsonResponse + { + $start = $request->getStart(); + $end = $request->getEnd(); + $assetAccounts = $request->getAssetAccounts(); + $result = []; + $expenses = $this->noRepository->sumIncome($start, $end, $assetAccounts); + /** @var array $expense */ + foreach ($expenses as $expense) { + $result[] = [ + 'difference' => $expense['sum'], + 'difference_float' => (float)$expense['sum'], + 'currency_id' => (string)$expense['currency_id'], + 'currency_code' => $expense['currency_code'], + ]; + } + + return response()->json($result); + + } +} \ No newline at end of file diff --git a/app/Api/V1/Controllers/Insight/Income/PeriodController.php b/app/Api/V1/Controllers/Insight/Income/PeriodController.php new file mode 100644 index 0000000000..bf43a4fba5 --- /dev/null +++ b/app/Api/V1/Controllers/Insight/Income/PeriodController.php @@ -0,0 +1,82 @@ +. + */ + +namespace FireflyIII\Api\V1\Controllers\Insight\Income; + + +use FireflyIII\Api\V1\Controllers\Controller; +use FireflyIII\Api\V1\Requests\Insight\GenericRequest; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; +use FireflyIII\Models\TransactionType; +use Illuminate\Http\JsonResponse; + +/** + * Class PeriodController + */ +class PeriodController extends Controller +{ + + /** + * @param GenericRequest $request + * + * @return JsonResponse + */ + public function total(GenericRequest $request): JsonResponse + { + $accounts = $request->getAssetAccounts(); + $start = $request->getStart(); + $end = $request->getEnd(); + $response = []; + + // collect all expenses in this period (regardless of type) + $collector = app(GroupCollectorInterface::class); + $collector->setTypes([TransactionType::DEPOSIT])->setRange($start, $end)->setDestinationAccounts($accounts); + $genericSet = $collector->getExtractedJournals(); + foreach ($genericSet as $journal) { + $currencyId = (int)$journal['currency_id']; + $foreignCurrencyId = (int)$journal['foreign_currency_id']; + + if (0 !== $currencyId) { + $response[$currencyId] = $response[$currencyId] ?? [ + 'difference' => '0', + 'difference_float' => 0, + 'currency_id' => (string)$currencyId, + 'currency_code' => $journal['currency_code'], + ]; + $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], app('steam')->positive($journal['amount'])); + $response[$currencyId]['difference_float'] = (float)$response[$currencyId]['difference']; + } + if (0 !== $foreignCurrencyId) { + $response[$foreignCurrencyId] = $response[$foreignCurrencyId] ?? [ + 'difference' => '0', + 'difference_float' => 0, + 'currency_id' => (string)$foreignCurrencyId, + 'currency_code' => $journal['foreign_currency_code'], + ]; + $response[$foreignCurrencyId]['difference'] = bcadd($response[$foreignCurrencyId]['difference'], app('steam')->positive($journal['foreign_amount'])); + $response[$foreignCurrencyId]['difference_float'] = (float)$response[$foreignCurrencyId]['difference']; + } + } + + return response()->json(array_values($response)); + } + +} \ No newline at end of file diff --git a/app/Api/V1/Controllers/Insight/Income/TagController.php b/app/Api/V1/Controllers/Insight/Income/TagController.php new file mode 100644 index 0000000000..eca5d01b4f --- /dev/null +++ b/app/Api/V1/Controllers/Insight/Income/TagController.php @@ -0,0 +1,173 @@ +. + */ + +namespace FireflyIII\Api\V1\Controllers\Insight\Income; + + +use FireflyIII\Api\V1\Controllers\Controller; +use FireflyIII\Api\V1\Requests\Insight\GenericRequest; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; +use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Tag\TagRepositoryInterface; +use Illuminate\Http\JsonResponse; + +/** + * Class TagController + */ +class TagController extends Controller +{ + private TagRepositoryInterface $repository; + + /** + * TagController constructor. + */ + public function __construct() + { + parent::__construct(); + $this->middleware( + function ($request, $next) { + $user = auth()->user(); + $this->repository = app(TagRepositoryInterface::class); + $this->repository->setUser($user); + + return $next($request); + } + ); + } + + /** + * Expenses per tag, possibly filtered by tag and account. + * + * @param GenericRequest $request + * + * @return JsonResponse + */ + public function tag(GenericRequest $request): JsonResponse + { + $accounts = $request->getAssetAccounts(); + $tags = $request->getTags(); + $start = $request->getStart(); + $end = $request->getEnd(); + $response = []; + + // get all tags: + if (0 === $tags->count()) { + $tags = $this->repository->get(); + } + + // collect all expenses in this period (regardless of type) by the given bills and accounts. + $collector = app(GroupCollectorInterface::class); + $collector->setTypes([TransactionType::DEPOSIT])->setRange($start, $end)->setDestinationAccounts($accounts); + $collector->setTags($tags); + $genericSet = $collector->getExtractedJournals(); + /** @var array $entry */ + foreach ($genericSet as $journal) { + $currencyId = (int)$journal['currency_id']; + $foreignCurrencyId = (int)$journal['foreign_currency_id']; + + /** @var array $tag */ + foreach ($journal['tags'] as $tag) { + $tagId = $tag['id']; + $key = sprintf('%d-%d', $tagId, $currencyId); + $foreignKey = sprintf('%d-%d', $tagId, $foreignCurrencyId); + + // on currency ID + if (0 !== $currencyId) { + $response[$key] = $response[$key] ?? [ + 'id' => (string)$tagId, + 'name' => $tag['name'], + 'difference' => '0', + 'difference_float' => 0, + 'currency_id' => (string)$currencyId, + 'currency_code' => $journal['currency_code'], + ]; + $response[$key]['difference'] = bcadd($response[$key]['difference'], app('steam')->positive($journal['amount'])); + $response[$key]['difference_float'] = (float)$response[$key]['difference']; + } + + // on foreign ID + if (0 !== $foreignCurrencyId) { + $response[$foreignKey] = $journal[$foreignKey] ?? [ + 'difference' => '0', + 'difference_float' => 0, + 'currency_id' => (string)$foreignCurrencyId, + 'currency_code' => $journal['foreign_currency_code'], + ]; + $response[$foreignKey]['difference'] = bcadd($response[$foreignKey]['difference'], app('steam')->positive($journal['foreign_amount'])); + $response[$foreignKey]['difference_float'] = (float)$response[$foreignKey]['difference']; + } + } + } + + return response()->json(array_values($response)); + } + + /** + * Expenses for no tag filtered by account. + * + * @param GenericRequest $request + * + * @return JsonResponse + */ + public function noTag(GenericRequest $request): JsonResponse + { + $accounts = $request->getAssetAccounts(); + $start = $request->getStart(); + $end = $request->getEnd(); + $response = []; + + // collect all expenses in this period (regardless of type) by the given bills and accounts. + $collector = app(GroupCollectorInterface::class); + $collector->setTypes([TransactionType::DEPOSIT])->setRange($start, $end)->setDestinationAccounts($accounts); + $collector->withoutTags(); + + $genericSet = $collector->getExtractedJournals(); + + foreach ($genericSet as $journal) { + $currencyId = (int)$journal['currency_id']; + $foreignCurrencyId = (int)$journal['foreign_currency_id']; + + if (0 !== $currencyId) { + $response[$currencyId] = $response[$currencyId] ?? [ + 'difference' => '0', + 'difference_float' => 0, + 'currency_id' => (string)$currencyId, + 'currency_code' => $journal['currency_code'], + ]; + $response[$currencyId]['difference'] = bcadd($response[$currencyId]['difference'], app('steam')->positive($journal['amount'])); + $response[$currencyId]['difference_float'] = (float)$response[$currencyId]['difference']; + } + if (0 !== $foreignCurrencyId) { + $response[$foreignCurrencyId] = $response[$foreignCurrencyId] ?? [ + 'difference' => '0', + 'difference_float' => 0, + 'currency_id' => (string)$foreignCurrencyId, + 'currency_code' => $journal['foreign_currency_code'], + ]; + $response[$foreignCurrencyId]['difference'] = bcadd($response[$foreignCurrencyId]['difference'], app('steam')->positive($journal['foreign_amount'])); + $response[$foreignCurrencyId]['difference_float'] = (float)$response[$foreignCurrencyId]['difference']; + } + } + + return response()->json(array_values($response)); + } + +} \ No newline at end of file diff --git a/app/Api/V1/Requests/Insight/ExpenseRequest.php b/app/Api/V1/Requests/Insight/GenericRequest.php similarity index 66% rename from app/Api/V1/Requests/Insight/ExpenseRequest.php rename to app/Api/V1/Requests/Insight/GenericRequest.php index e6005e7ef7..ce67059a71 100644 --- a/app/Api/V1/Requests/Insight/ExpenseRequest.php +++ b/app/Api/V1/Requests/Insight/GenericRequest.php @@ -8,23 +8,27 @@ use Carbon\Carbon; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; +use FireflyIII\Repositories\Tag\TagRepositoryInterface; use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ConvertsDataTypes; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Collection; /** - * Class ExpenseRequest + * Class GenericRequest */ -class ExpenseRequest extends FormRequest +class GenericRequest extends FormRequest { use ConvertsDataTypes, ChecksLogin; private Collection $accounts; private Collection $budgets; private Collection $categories; + private Collection $bills; + private Collection $tags; /** * @return Carbon @@ -48,31 +52,6 @@ class ExpenseRequest extends FormRequest return $date; } - /** - * - */ - private function parseAccounts(): void - { - if (null === $this->accounts) { - $this->accounts = new Collection; - } - if (0 !== $this->accounts->count()) { - return; - } - $repository = app(AccountRepositoryInterface::class); - $repository->setUser(auth()->user()); - $array = $this->get('accounts'); - if (is_array($array)) { - foreach ($array as $accountId) { - $accountId = (int)$accountId; - $account = $repository->findNull($accountId); - if (null !== $account) { - $this->accounts->push($account); - } - } - } - } - /** * */ @@ -118,6 +97,26 @@ class ExpenseRequest extends FormRequest return $this->categories; } + /** + * @return Collection + */ + public function getBills(): Collection + { + $this->parseBills(); + + return $this->bills; + } + + /** + * @return Collection + */ + public function getTags(): Collection + { + $this->parseTags(); + + return $this->tags; + } + /** * @return Collection @@ -155,6 +154,24 @@ class ExpenseRequest extends FormRequest return $return; } + /** + * @return Collection + */ + public function getRevenueAccounts(): Collection + { + $this->parseAccounts(); + $return = new Collection; + /** @var Account $account */ + foreach ($this->accounts as $account) { + $type = $account->accountType->type; + if (in_array($type, [AccountType::REVENUE])) { + $return->push($account); + } + } + + return $return; + } + /** * Get all data from the request. * @@ -176,9 +193,11 @@ class ExpenseRequest extends FormRequest public function rules(): array { // this is cheating but it works: - $this->accounts = new Collection; - $this->budgets = new Collection; + $this->accounts = new Collection; + $this->budgets = new Collection; $this->categories = new Collection; + $this->bills = new Collection; + $this->tags = new Collection; return [ 'start' => 'required|date', @@ -186,6 +205,56 @@ class ExpenseRequest extends FormRequest ]; } + /** + * + */ + private function parseAccounts(): void + { + if (null === $this->accounts) { + $this->accounts = new Collection; + } + if (0 !== $this->accounts->count()) { + return; + } + $repository = app(AccountRepositoryInterface::class); + $repository->setUser(auth()->user()); + $array = $this->get('accounts'); + if (is_array($array)) { + foreach ($array as $accountId) { + $accountId = (int)$accountId; + $account = $repository->findNull($accountId); + if (null !== $account) { + $this->accounts->push($account); + } + } + } + } + + /** + * + */ + private function parseBills(): void + { + if (null === $this->bills) { + $this->bills = new Collection; + } + if (0 !== $this->bills->count()) { + return; + } + $repository = app(BillRepositoryInterface::class); + $repository->setUser(auth()->user()); + $array = $this->get('bills'); + if (is_array($array)) { + foreach ($array as $billId) { + $billId = (int)$billId; + $bill = $repository->findNull($billId); + if (null !== $billId) { + $this->bills->push($bill); + } + } + } + } + /** * */ @@ -210,4 +279,29 @@ class ExpenseRequest extends FormRequest } } } + + /** + * + */ + private function parseTags(): void + { + if (null === $this->tags) { + $this->tags = new Collection; + } + if (0 !== $this->tags->count()) { + return; + } + $repository = app(TagRepositoryInterface::class); + $repository->setUser(auth()->user()); + $array = $this->get('tags'); + if (is_array($array)) { + foreach ($array as $tagId) { + $tagId = (int)$tagId; + $tag = $repository->findNull($tagId); + if (null !== $tagId) { + $this->tags->push($tag); + } + } + } + } } \ No newline at end of file diff --git a/app/Helpers/Collector/Extensions/MetaCollection.php b/app/Helpers/Collector/Extensions/MetaCollection.php index fb09f619b3..c176fc2687 100644 --- a/app/Helpers/Collector/Extensions/MetaCollection.php +++ b/app/Helpers/Collector/Extensions/MetaCollection.php @@ -369,6 +369,18 @@ trait MetaCollection return $this; } + /** + * Limit results to a transactions without a bill. + * + * @return GroupCollectorInterface + */ + public function withoutBill(): GroupCollectorInterface + { + $this->query->whereNull('transaction_journals.bill_id'); + + return $this; + } + /** * Limit results to a transactions without a budget.. * diff --git a/app/Helpers/Collector/GroupCollectorInterface.php b/app/Helpers/Collector/GroupCollectorInterface.php index b0dbe45856..43493c59ed 100644 --- a/app/Helpers/Collector/GroupCollectorInterface.php +++ b/app/Helpers/Collector/GroupCollectorInterface.php @@ -501,6 +501,13 @@ interface GroupCollectorInterface */ public function withoutBudget(): GroupCollectorInterface; + /** + * Limit results to a transactions without a bill. + * + * @return GroupCollectorInterface + */ + public function withoutBill(): GroupCollectorInterface; + /** * Limit results to a transactions without a category. * diff --git a/app/Repositories/Account/OperationsRepository.php b/app/Repositories/Account/OperationsRepository.php index 5019b146df..2525e88b23 100644 --- a/app/Repositories/Account/OperationsRepository.php +++ b/app/Repositories/Account/OperationsRepository.php @@ -38,8 +38,7 @@ use Log; */ class OperationsRepository implements OperationsRepositoryInterface { - /** @var User */ - private $user; + private User $user; /** * This method returns a list of all the withdrawal transaction journals (as arrays) set in that period @@ -192,11 +191,15 @@ class OperationsRepository implements OperationsRepositoryInterface /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setUser($this->user)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL]) - ->setForeignCurrency($currency)->setBudgets($budgets); + ->setForeignCurrency($currency); if (null !== $accounts) { - $collector->setAccounts($accounts); + $collector->setSourceAccounts($accounts); } + if (null !== $expense) { + $collector->setDestinationAccounts($expense); + } + $result = $collector->getExtractedJournals(); // do not use array_merge because you want keys to overwrite (otherwise you get double results): @@ -234,16 +237,78 @@ class OperationsRepository implements OperationsRepositoryInterface } /** - * Sum of income journals in period for a set of accounts, grouped per currency. Amounts are always positive. - * - * @param Carbon $start - * @param Carbon $end - * @param Collection|null $accounts - * - * @return array + * // TODO same as income but copied. + * @inheritDoc */ - public function sumIncome(Carbon $start, Carbon $end, ?Collection $accounts = null): array + public function sumIncome(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $revenue = null, ?TransactionCurrency $currency = null): array { - throw new FireflyException(sprintf('%s is not yet implemented', __METHOD__)); + $start->startOfDay(); + $end->endOfDay(); + + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector + ->setUser($this->user) + ->setRange($start, $end) + ->setTypes([TransactionType::DEPOSIT]); + + if (null !== $accounts) { + $collector->setDestinationAccounts($accounts); + } + if(null !== $revenue) { + $collector->setSourceAccounts($revenue); + } + if (null !== $currency) { + $collector->setCurrency($currency); + } + $journals = $collector->getExtractedJournals(); + + // same but for foreign currencies: + if (null !== $currency) { + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setUser($this->user)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL]) + ->setForeignCurrency($currency); + + if (null !== $accounts) { + $collector->setDestinationAccounts($accounts); + } + if(null !== $revenue) { + $collector->setSourceAccounts($revenue); + } + $result = $collector->getExtractedJournals(); + + // do not use array_merge because you want keys to overwrite (otherwise you get double results): + $journals = $result + $journals; + } + $array = []; + + foreach ($journals as $journal) { + $currencyId = (int)$journal['currency_id']; + $array[$currencyId] = $array[$currencyId] ?? [ + 'sum' => '0', + 'currency_id' => $currencyId, + 'currency_name' => $journal['currency_name'], + 'currency_symbol' => $journal['currency_symbol'], + 'currency_code' => $journal['currency_code'], + 'currency_decimal_places' => $journal['currency_decimal_places'], + ]; + $array[$currencyId]['sum'] = bcadd($array[$currencyId]['sum'], app('steam')->positive($journal['amount'])); + + // also do foreign amount: + $foreignId = (int)$journal['foreign_currency_id']; + if (0 !== $foreignId) { + $array[$foreignId] = $array[$foreignId] ?? [ + 'sum' => '0', + 'currency_id' => $foreignId, + 'currency_name' => $journal['foreign_currency_name'], + 'currency_symbol' => $journal['foreign_currency_symbol'], + 'currency_code' => $journal['foreign_currency_code'], + 'currency_decimal_places' => $journal['foreign_currency_decimal_places'], + ]; + $array[$foreignId]['sum'] = bcadd($array[$foreignId]['sum'], app('steam')->positive($journal['foreign_amount'])); + } + } + return $array; } } diff --git a/app/Repositories/Account/OperationsRepositoryInterface.php b/app/Repositories/Account/OperationsRepositoryInterface.php index b8404113aa..e0cdeed9ec 100644 --- a/app/Repositories/Account/OperationsRepositoryInterface.php +++ b/app/Repositories/Account/OperationsRepositoryInterface.php @@ -80,11 +80,13 @@ interface OperationsRepositoryInterface /** * Sum of income journals in period for a set of accounts, grouped per currency. Amounts are always positive. * - * @param Carbon $start - * @param Carbon $end - * @param Collection|null $accounts + * @param Carbon $start + * @param Carbon $end + * @param Collection|null $accounts + * @param Collection|null $revenue + * @param TransactionCurrency|null $currency * * @return array */ - public function sumIncome(Carbon $start, Carbon $end, ?Collection $accounts = null): array; + public function sumIncome(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $revenue = null, ?TransactionCurrency $currency = null): array; } diff --git a/resources/views/v2/bills/index.twig b/resources/views/v2/bills/index_x.twig similarity index 100% rename from resources/views/v2/bills/index.twig rename to resources/views/v2/bills/index_x.twig diff --git a/routes/api.php b/routes/api.php index e9415e9502..c5228dd835 100644 --- a/routes/api.php +++ b/routes/api.php @@ -95,19 +95,14 @@ Route::group( static function () { // Insight in expenses per account: Route::get('expense', ['uses' => 'AccountController@expense', 'as' => 'expense']); - Route::get('asset', ['uses' => 'AccountController@asset', 'as' => 'asset']); + Route::get('asset', ['uses' => 'AccountController@asset', 'as' => 'asset']); Route::get('total', ['uses' => 'PeriodController@total', 'as' => 'total']); - - // TODO bill and no bill + asset account. Route::get('bill', ['uses' => 'BillController@bill', 'as' => 'bill']); Route::get('no-bill', ['uses' => 'BillController@noBill', 'as' => 'no-bill']); - Route::get('budget', ['uses' => 'BudgetController@budget', 'as' => 'budget']); Route::get('no-budget', ['uses' => 'BudgetController@noBudget', 'as' => 'no-budget']); Route::get('category', ['uses' => 'CategoryController@category', 'as' => 'category']); Route::get('no-category', ['uses' => 'CategoryController@noCategory', 'as' => 'no-category']); - - // TODO per tag + asset account Route::get('tag', ['uses' => 'TagController@tag', 'as' => 'tag']); Route::get('no-tag', ['uses' => 'TagController@noTag', 'as' => 'no-tag']); @@ -115,61 +110,41 @@ Route::group( // TODO per budget limit? // TODO per object group? // TODO per recurrence? - - - - - + // TODO per object group + // TODO transfers voor piggies + // TODO transfers per piggy? + // TODO currency? + // TODO net worth? } ); - -// Insight in expenses: +// insight in income Route::group( ['namespace' => 'FireflyIII\Api\V1\Controllers\Insight\Income', 'prefix' => 'insight/income', 'as' => 'api.v1.insight.income.',], static function () { - // Insight in income - - // TODO grouped by expense account or asset account: - Route::get('revenue', ['uses' => 'AccountController@revenue', 'as' => 'expense']); - Route::get('asset', ['uses' => 'AccountController@asset', 'as' => 'asset']); - - // TODO total: + // Insight in expenses per account: + Route::get('revenue', ['uses' => 'AccountController@revenue', 'as' => 'revenue']); + Route::get('asset', ['uses' => 'AccountController@asset', 'as' => 'asset']); Route::get('total', ['uses' => 'PeriodController@total', 'as' => 'total']); - - // TODO category and no category Route::get('category', ['uses' => 'CategoryController@category', 'as' => 'category']); Route::get('no-category', ['uses' => 'CategoryController@noCategory', 'as' => 'no-category']); + Route::get('tag', ['uses' => 'TagController@tag', 'as' => 'tag']); + Route::get('no-tag', ['uses' => 'TagController@noTag', 'as' => 'no-tag']); + + // TODO per budget limit? + // TODO per object group? + // TODO per recurrence? + // TODO per object group + // TODO transfers voor piggies + // TODO transfers per piggy? + // TODO currency? + // TODO net worth? } ); // Insight in transfers -// TODO transfers per piggy? - - - - - - - - - - - - - - - - - - - - - - - /** @@ -203,16 +178,6 @@ Route::group( */ - - - - - - - - - - /** * System and configuration controllers */ @@ -282,7 +247,6 @@ Route::group( ); - // TODO VERIFY API DOCS Route::group( ['namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'groups', @@ -337,7 +301,7 @@ Route::group( 'as' => 'api.v1.bills.',], static function () { - // Bills API routes: + // Bills API routes: Route::get('', ['uses' => 'BillController@index', 'as' => 'index']); Route::post('', ['uses' => 'BillController@store', 'as' => 'store']); Route::get('{bill}', ['uses' => 'BillController@show', 'as' => 'show']); @@ -406,7 +370,6 @@ Route::group( ); - // TODO VERIFY API DOCS Route::group( ['namespace' => 'FireflyIII\Api\V1\Controllers', 'prefix' => 'cer', @@ -569,7 +532,6 @@ Route::group( ); - // destroy data route. // TODO VERIFY API DOCS Route::group(