mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-02-25 18:45:27 -06:00
Merge branch 'release/v6.0.20'
This commit is contained in:
commit
0a7a099796
60
.ci/php-cs-fixer/composer.lock
generated
60
.ci/php-cs-fixer/composer.lock
generated
@ -745,16 +745,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v6.3.0",
|
||||
"version": "v6.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
"reference": "8788808b07cf0bdd6e4b7fdd23d8ddb1470c83b7"
|
||||
"reference": "aa5d64ad3f63f2e48964fc81ee45cb318a723898"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/8788808b07cf0bdd6e4b7fdd23d8ddb1470c83b7",
|
||||
"reference": "8788808b07cf0bdd6e4b7fdd23d8ddb1470c83b7",
|
||||
"url": "https://api.github.com/repos/symfony/console/zipball/aa5d64ad3f63f2e48964fc81ee45cb318a723898",
|
||||
"reference": "aa5d64ad3f63f2e48964fc81ee45cb318a723898",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -815,7 +815,7 @@
|
||||
"terminal"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/console/tree/v6.3.0"
|
||||
"source": "https://github.com/symfony/console/tree/v6.3.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -831,7 +831,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-05-29T12:49:39+00:00"
|
||||
"time": "2023-07-19T20:17:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/deprecation-contracts",
|
||||
@ -902,16 +902,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher",
|
||||
"version": "v6.3.0",
|
||||
"version": "v6.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/event-dispatcher.git",
|
||||
"reference": "3af8ac1a3f98f6dbc55e10ae59c9e44bfc38dfaa"
|
||||
"reference": "adb01fe097a4ee930db9258a3cc906b5beb5cf2e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/3af8ac1a3f98f6dbc55e10ae59c9e44bfc38dfaa",
|
||||
"reference": "3af8ac1a3f98f6dbc55e10ae59c9e44bfc38dfaa",
|
||||
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/adb01fe097a4ee930db9258a3cc906b5beb5cf2e",
|
||||
"reference": "adb01fe097a4ee930db9258a3cc906b5beb5cf2e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -962,7 +962,7 @@
|
||||
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/event-dispatcher/tree/v6.3.0"
|
||||
"source": "https://github.com/symfony/event-dispatcher/tree/v6.3.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -978,7 +978,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-04-21T14:41:17+00:00"
|
||||
"time": "2023-07-06T06:56:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher-contracts",
|
||||
@ -1121,16 +1121,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v6.3.0",
|
||||
"version": "v6.3.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "d9b01ba073c44cef617c7907ce2419f8d00d75e2"
|
||||
"reference": "9915db259f67d21eefee768c1abcf1cc61b1fc9e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/d9b01ba073c44cef617c7907ce2419f8d00d75e2",
|
||||
"reference": "d9b01ba073c44cef617c7907ce2419f8d00d75e2",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/9915db259f67d21eefee768c1abcf1cc61b1fc9e",
|
||||
"reference": "9915db259f67d21eefee768c1abcf1cc61b1fc9e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -1165,7 +1165,7 @@
|
||||
"description": "Finds files and directories via an intuitive fluent interface",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/finder/tree/v6.3.0"
|
||||
"source": "https://github.com/symfony/finder/tree/v6.3.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -1181,7 +1181,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-04-02T01:25:41+00:00"
|
||||
"time": "2023-07-31T08:31:44+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/options-resolver",
|
||||
@ -1744,16 +1744,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v6.3.0",
|
||||
"version": "v6.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
"reference": "8741e3ed7fe2e91ec099e02446fb86667a0f1628"
|
||||
"reference": "c5ce962db0d9b6e80247ca5eb9af6472bd4d7b5d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/8741e3ed7fe2e91ec099e02446fb86667a0f1628",
|
||||
"reference": "8741e3ed7fe2e91ec099e02446fb86667a0f1628",
|
||||
"url": "https://api.github.com/repos/symfony/process/zipball/c5ce962db0d9b6e80247ca5eb9af6472bd4d7b5d",
|
||||
"reference": "c5ce962db0d9b6e80247ca5eb9af6472bd4d7b5d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -1785,7 +1785,7 @@
|
||||
"description": "Executes commands in sub-processes",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/process/tree/v6.3.0"
|
||||
"source": "https://github.com/symfony/process/tree/v6.3.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -1801,7 +1801,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-05-19T08:06:44+00:00"
|
||||
"time": "2023-07-12T16:00:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/service-contracts",
|
||||
@ -1949,16 +1949,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/string",
|
||||
"version": "v6.3.0",
|
||||
"version": "v6.3.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/string.git",
|
||||
"reference": "f2e190ee75ff0f5eced645ec0be5c66fac81f51f"
|
||||
"reference": "53d1a83225002635bca3482fcbf963001313fb68"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/f2e190ee75ff0f5eced645ec0be5c66fac81f51f",
|
||||
"reference": "f2e190ee75ff0f5eced645ec0be5c66fac81f51f",
|
||||
"url": "https://api.github.com/repos/symfony/string/zipball/53d1a83225002635bca3482fcbf963001313fb68",
|
||||
"reference": "53d1a83225002635bca3482fcbf963001313fb68",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -2015,7 +2015,7 @@
|
||||
"utf8"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/string/tree/v6.3.0"
|
||||
"source": "https://github.com/symfony/string/tree/v6.3.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -2031,7 +2031,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2023-03-21T21:06:29+00:00"
|
||||
"time": "2023-07-05T08:41:27+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
|
18
.editorconfig
Normal file
18
.editorconfig
Normal file
@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[docker-compose.yml]
|
||||
indent_size = 4
|
17
.gitattributes
vendored
17
.gitattributes
vendored
@ -1,8 +1,11 @@
|
||||
* text=auto
|
||||
*.css linguist-vendored
|
||||
*.scss linguist-vendored
|
||||
*.js linguist-vendored
|
||||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
/tests export-ignore
|
||||
/phpunit.xml export-ignore
|
||||
/.ci export-ignore
|
||||
.styleci.yml export-ignore
|
||||
|
@ -78,7 +78,8 @@ class UpdateController extends Controller
|
||||
public function update(UpdateRequest $request, TransactionGroup $transactionGroup): JsonResponse
|
||||
{
|
||||
Log::debug('Now in update routine for transaction group!');
|
||||
$data = $request->getAll();
|
||||
$data = $request->getAll();
|
||||
|
||||
$transactionGroup = $this->groupRepository->update($transactionGroup, $data);
|
||||
$manager = $this->getManager();
|
||||
|
||||
|
@ -322,16 +322,11 @@ class BasicController extends Controller
|
||||
'currency_decimal_places' => $row['currency_decimal_places'],
|
||||
'value_parsed' => app('amount')->formatFlat($row['currency_symbol'], $row['currency_decimal_places'], $leftToSpend, false),
|
||||
'local_icon' => 'money',
|
||||
'sub_title' => (string)trans(
|
||||
'firefly.box_spend_per_day',
|
||||
[
|
||||
'amount' => app('amount')->formatFlat(
|
||||
$row['currency_symbol'],
|
||||
$row['currency_decimal_places'],
|
||||
$perDay,
|
||||
false
|
||||
),
|
||||
]
|
||||
'sub_title' => app('amount')->formatFlat(
|
||||
$row['currency_symbol'],
|
||||
$row['currency_decimal_places'],
|
||||
$perDay,
|
||||
false
|
||||
),
|
||||
];
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ class CronController extends Controller
|
||||
$return = [];
|
||||
$return['recurring_transactions'] = $this->runRecurring($config['force'], $config['date']);
|
||||
$return['auto_budgets'] = $this->runAutoBudget($config['force'], $config['date']);
|
||||
if (true === config('cer.enabled')) {
|
||||
if (true === config('cer.download_enabled')) {
|
||||
$return['exchange_rates'] = $this->exchangeRatesCronJob($config['force'], $config['date']);
|
||||
}
|
||||
$return['bill_warnings'] = $this->billWarningCronJob($config['force'], $config['date']);
|
||||
|
@ -33,7 +33,6 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Repositories\Administration\Account\AccountRepositoryInterface as AdminAccountRepositoryInterface;
|
||||
use FireflyIII\Support\Http\Api\AccountFilter;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use JsonException;
|
||||
|
||||
/**
|
||||
* Class AccountController
|
||||
@ -65,12 +64,16 @@ class AccountController extends Controller
|
||||
|
||||
/**
|
||||
* Documentation for this endpoint:
|
||||
* TODO endpoint is not documented.
|
||||
* TODO list of checks
|
||||
* 1. use dates from ParameterBag
|
||||
* 2. Request validates dates
|
||||
* 3. Request includes user_group_id as administration_id
|
||||
* 4. Endpoint is documented.
|
||||
* 5. Collector uses administration_id
|
||||
*
|
||||
* @param AutocompleteRequest $request
|
||||
*
|
||||
* @return JsonResponse
|
||||
* @throws JsonException
|
||||
* @throws FireflyException
|
||||
* @throws FireflyException
|
||||
*/
|
||||
@ -79,7 +82,7 @@ class AccountController extends Controller
|
||||
$data = $request->getData();
|
||||
$types = $data['types'];
|
||||
$query = $data['query'];
|
||||
$date = $data['date'] ?? today(config('app.timezone'));
|
||||
$date = $this->parameters->get('date') ?? today(config('app.timezone'));
|
||||
$this->adminRepository->setAdministrationId($data['administration_id']);
|
||||
|
||||
$return = [];
|
||||
|
@ -27,11 +27,12 @@ namespace FireflyIII\Api\V2\Controllers\Chart;
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Api\V2\Controllers\Controller;
|
||||
use FireflyIII\Api\V2\Request\Generic\DateRequest;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Repositories\Administration\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Support\Http\Api\ConvertsExchangeRates;
|
||||
use FireflyIII\User;
|
||||
use FireflyIII\Support\Http\Api\CleansChartData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
@ -41,7 +42,7 @@ use Psr\Container\NotFoundExceptionInterface;
|
||||
*/
|
||||
class AccountController extends Controller
|
||||
{
|
||||
use ConvertsExchangeRates;
|
||||
use CleansChartData;
|
||||
|
||||
private AccountRepositoryInterface $repository;
|
||||
|
||||
@ -54,7 +55,7 @@ class AccountController extends Controller
|
||||
$this->middleware(
|
||||
function ($request, $next) {
|
||||
$this->repository = app(AccountRepositoryInterface::class);
|
||||
|
||||
$this->repository->setAdministrationId(auth()->user()->user_group_id);
|
||||
return $next($request);
|
||||
}
|
||||
);
|
||||
@ -66,39 +67,38 @@ class AccountController extends Controller
|
||||
*
|
||||
* The native currency is the preferred currency on the page /currencies.
|
||||
*
|
||||
* If a transaction has foreign currency = native currency, the foreign amount will be used, no conversion
|
||||
* will take place.
|
||||
*
|
||||
* TODO validate and set administration_id from request
|
||||
*
|
||||
* @param DateRequest $request
|
||||
*
|
||||
* @return JsonResponse
|
||||
* @throws ContainerExceptionInterface
|
||||
* @throws NotFoundExceptionInterface
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function dashboard(DateRequest $request): JsonResponse
|
||||
{
|
||||
// parameters for chart:
|
||||
$dates = $request->getAll();
|
||||
/** @var Carbon $start */
|
||||
$start = $dates['start'];
|
||||
$start = $this->parameters->get('start');
|
||||
/** @var Carbon $end */
|
||||
$end = $dates['end'];
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
// group ID
|
||||
$administrationId = $user->getAdministrationId();
|
||||
$this->repository->setAdministrationId($administrationId);
|
||||
$end = $this->parameters->get('end');
|
||||
$end->endOfDay();
|
||||
|
||||
// user's preferences
|
||||
$defaultSet = $this->repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT])->pluck('id')->toArray();
|
||||
$frontPage = app('preferences')->get('frontPageAccounts', $defaultSet);
|
||||
$default = app('amount')->getDefaultCurrency();
|
||||
$accounts = $this->repository->getAccountsById($frontPage->data);
|
||||
$chartData = [];
|
||||
/** @var TransactionCurrency $default */
|
||||
$default = app('amount')->getDefaultCurrency();
|
||||
$accounts = $this->repository->getAccountsById($frontPage->data);
|
||||
$chartData = [];
|
||||
|
||||
if (!(is_array($frontPage->data) && count($frontPage->data) > 0)) {
|
||||
$frontPage->data = $defaultSet;
|
||||
$frontPage->save();
|
||||
}
|
||||
|
||||
/** @var Account $account */
|
||||
foreach ($accounts as $account) {
|
||||
$currency = $this->repository->getAccountCurrency($account);
|
||||
@ -113,15 +113,16 @@ class AccountController extends Controller
|
||||
'currency_symbol' => $currency->symbol,
|
||||
'currency_decimal_places' => $currency->decimal_places,
|
||||
|
||||
// the default currency of the user (may be the same!)
|
||||
'native_id' => $default->id,
|
||||
// the default currency of the user (could be the same!)
|
||||
'native_id' => (int)$default->id,
|
||||
'native_code' => $default->code,
|
||||
'native_symbol' => $default->symbol,
|
||||
'native_decimal_places' => $default->decimal_places,
|
||||
'start_date' => $start->toAtomString(),
|
||||
'end_date' => $end->toAtomString(),
|
||||
'native_decimal_places' => (int)$default->decimal_places,
|
||||
'start' => $start->toAtomString(),
|
||||
'end' => $end->toAtomString(),
|
||||
'period' => '1D',
|
||||
'entries' => [],
|
||||
'converted_entries' => [],
|
||||
'native_entries' => [],
|
||||
];
|
||||
$currentStart = clone $start;
|
||||
$range = app('steam')->balanceInRange($account, $start, clone $end, $currency);
|
||||
@ -138,13 +139,13 @@ class AccountController extends Controller
|
||||
$previousConverted = $balanceConverted;
|
||||
|
||||
$currentStart->addDay();
|
||||
$currentSet['entries'][$label] = $balance;
|
||||
$currentSet['converted_entries'][$label] = $balanceConverted;
|
||||
$currentSet['entries'][$label] = $balance;
|
||||
$currentSet['native_entries'][$label] = $balanceConverted;
|
||||
}
|
||||
$currentSet = $this->cerChartSet($currentSet);
|
||||
$chartData[] = $currentSet;
|
||||
}
|
||||
|
||||
return response()->json($chartData);
|
||||
return response()->json($this->clean($chartData));
|
||||
}
|
||||
|
||||
}
|
||||
|
260
app/Api/V2/Controllers/Chart/BalanceController.php
Normal file
260
app/Api/V2/Controllers/Chart/BalanceController.php
Normal file
@ -0,0 +1,260 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* BalanceController.php
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Api\V2\Controllers\Chart;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Api\V2\Controllers\Controller;
|
||||
use FireflyIII\Api\V2\Request\Chart\BalanceChartRequest;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use FireflyIII\Repositories\Administration\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Support\Http\Api\CleansChartData;
|
||||
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class BalanceController
|
||||
*/
|
||||
class BalanceController extends Controller
|
||||
{
|
||||
use CleansChartData;
|
||||
|
||||
private AccountRepositoryInterface $repository;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->middleware(
|
||||
function ($request, $next) {
|
||||
$this->repository = app(AccountRepositoryInterface::class);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The code is practically a duplicate of ReportController::operations.
|
||||
*
|
||||
* Currency is up to the account/transactions in question, but conversion to the default
|
||||
* currency is possible.
|
||||
*
|
||||
* If the transaction being processed is already in native currency OR if the
|
||||
* foreign amount is in the native currency, the amount will not be converted.
|
||||
*
|
||||
* TODO validate and set administration_id
|
||||
* TODO collector set group, not user
|
||||
*
|
||||
* @param BalanceChartRequest $request
|
||||
*
|
||||
* @return JsonResponse
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function balance(BalanceChartRequest $request): JsonResponse
|
||||
{
|
||||
$params = $request->getAll();
|
||||
/** @var Carbon $start */
|
||||
$start = $this->parameters->get('start');
|
||||
/** @var Carbon $end */
|
||||
$end = $this->parameters->get('end');
|
||||
$end->endOfDay();
|
||||
/** @var Collection $accounts */
|
||||
$accounts = $params['accounts'];
|
||||
$preferredRange = $params['period'];
|
||||
|
||||
// set some formats, based on input parameters.
|
||||
$format = app('navigation')->preferredCarbonFormatByPeriod($preferredRange);
|
||||
|
||||
// prepare for currency conversion and data collection:
|
||||
$ids = $accounts->pluck('id')->toArray();
|
||||
/** @var TransactionCurrency $default */
|
||||
$default = app('amount')->getDefaultCurrency();
|
||||
$converter = new ExchangeRateConverter();
|
||||
$currencies = [(int)$default->id => $default,]; // currency cache
|
||||
$data = [];
|
||||
$chartData = [];
|
||||
|
||||
// get journals for entire period:
|
||||
/** @var GroupCollectorInterface $collector */
|
||||
$collector = app(GroupCollectorInterface::class);
|
||||
$collector->setRange($start, $end)->withAccountInformation();
|
||||
$collector->setXorAccounts($accounts);
|
||||
$collector->setTypes([TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::RECONCILIATION, TransactionType::TRANSFER]);
|
||||
$journals = $collector->getExtractedJournals();
|
||||
|
||||
// set array for default currency (even if unused later on)
|
||||
$defaultCurrencyId = (int)$default->id;
|
||||
$data[$defaultCurrencyId] = [
|
||||
'currency_id' => $defaultCurrencyId,
|
||||
'currency_symbol' => $default->symbol,
|
||||
'currency_code' => $default->code,
|
||||
'currency_name' => $default->name,
|
||||
'currency_decimal_places' => (int)$default->decimal_places,
|
||||
'native_id' => $defaultCurrencyId,
|
||||
'native_symbol' => $default->symbol,
|
||||
'native_code' => $default->code,
|
||||
'native_name' => $default->name,
|
||||
'native_decimal_places' => (int)$default->decimal_places,
|
||||
];
|
||||
|
||||
|
||||
// loop. group by currency and by period.
|
||||
/** @var array $journal */
|
||||
foreach ($journals as $journal) {
|
||||
// format the date according to the period
|
||||
$period = $journal['date']->format($format);
|
||||
|
||||
// collect (and cache) currency information for this journal.
|
||||
$currencyId = (int)$journal['currency_id'];
|
||||
$currency = $currencies[$currencyId] ?? TransactionCurrency::find($currencyId);
|
||||
$currencies[$currencyId] = $currency; // may just re-assign itself, don't mind.
|
||||
|
||||
// set the array with monetary info, if it does not exist.
|
||||
$data[$currencyId] = $data[$currencyId] ?? [
|
||||
'currency_id' => $currencyId,
|
||||
'currency_symbol' => $journal['currency_symbol'],
|
||||
'currency_code' => $journal['currency_code'],
|
||||
'currency_name' => $journal['currency_name'],
|
||||
'currency_decimal_places' => $journal['currency_decimal_places'],
|
||||
// native currency info (could be the same)
|
||||
'native_id' => (int)$default->id,
|
||||
'native_code' => $default->code,
|
||||
'native_symbol' => $default->symbol,
|
||||
'native_decimal_places' => (int)$default->decimal_places,
|
||||
];
|
||||
|
||||
// set the array (in monetary info) with spent/earned in this $period, if it does not exist.
|
||||
$data[$currencyId][$period] = $data[$currencyId][$period] ?? [
|
||||
'period' => $period,
|
||||
'spent' => '0',
|
||||
'earned' => '0',
|
||||
'native_spent' => '0',
|
||||
'native_earned' => '0',
|
||||
];
|
||||
// is this journal's amount in- our outgoing?
|
||||
$key = 'spent';
|
||||
$amount = app('steam')->negative($journal['amount']);
|
||||
// deposit = incoming
|
||||
// transfer or reconcile or opening balance, and these accounts are the destination.
|
||||
if (
|
||||
TransactionType::DEPOSIT === $journal['transaction_type_type']
|
||||
||
|
||||
|
||||
(
|
||||
(
|
||||
TransactionType::TRANSFER === $journal['transaction_type_type']
|
||||
|| TransactionType::RECONCILIATION === $journal['transaction_type_type']
|
||||
|| TransactionType::OPENING_BALANCE === $journal['transaction_type_type']
|
||||
)
|
||||
&& in_array($journal['destination_account_id'], $ids, true)
|
||||
)
|
||||
) {
|
||||
$key = 'earned';
|
||||
$amount = app('steam')->positive($journal['amount']);
|
||||
}
|
||||
// get conversion rate
|
||||
$rate = $converter->getCurrencyRate($currency, $default, $journal['date']);
|
||||
$amountConverted = bcmul($amount, $rate);
|
||||
|
||||
// perhaps transaction already has the foreign amount in the native currency.
|
||||
if ((int)$journal['foreign_currency_id'] === (int)$default->id) {
|
||||
$amountConverted = $journal['foreign_amount'] ?? '0';
|
||||
$amountConverted = 'earned' === $key ? app('steam')->positive($amountConverted) : app('steam')->negative($amountConverted);
|
||||
}
|
||||
|
||||
// add normal entry
|
||||
$data[$currencyId][$period][$key] = bcadd($data[$currencyId][$period][$key], $amount);
|
||||
|
||||
// add converted entry
|
||||
$convertedKey = sprintf('native_%s', $key);
|
||||
$data[$currencyId][$period][$convertedKey] = bcadd($data[$currencyId][$period][$convertedKey], $amountConverted);
|
||||
}
|
||||
|
||||
// loop this data, make chart bars for each currency:
|
||||
/** @var array $currency */
|
||||
foreach ($data as $currency) {
|
||||
// income and expense array prepped:
|
||||
$income = [
|
||||
'label' => 'earned',
|
||||
'currency_id' => $currency['currency_id'],
|
||||
'currency_symbol' => $currency['currency_symbol'],
|
||||
'currency_code' => $currency['currency_code'],
|
||||
'currency_decimal_places' => $currency['currency_decimal_places'],
|
||||
'native_id' => $currency['native_id'],
|
||||
'native_symbol' => $currency['native_symbol'],
|
||||
'native_code' => $currency['native_code'],
|
||||
'native_decimal_places' => $currency['native_decimal_places'],
|
||||
'start' => $start->toAtomString(),
|
||||
'end' => $end->toAtomString(),
|
||||
'period' => $preferredRange,
|
||||
'entries' => [],
|
||||
'native_entries' => [],
|
||||
];
|
||||
$expense = [
|
||||
'label' => 'spent',
|
||||
'currency_id' => $currency['currency_id'],
|
||||
'currency_symbol' => $currency['currency_symbol'],
|
||||
'currency_code' => $currency['currency_code'],
|
||||
'currency_decimal_places' => $currency['currency_decimal_places'],
|
||||
'native_id' => $currency['native_id'],
|
||||
'native_symbol' => $currency['native_symbol'],
|
||||
'native_code' => $currency['native_code'],
|
||||
'native_decimal_places' => $currency['native_decimal_places'],
|
||||
'start' => $start->toAtomString(),
|
||||
'end' => $end->toAtomString(),
|
||||
'period' => $preferredRange,
|
||||
'entries' => [],
|
||||
'native_entries' => [],
|
||||
|
||||
];
|
||||
// loop all possible periods between $start and $end, and add them to the correct dataset.
|
||||
$currentStart = clone $start;
|
||||
while ($currentStart <= $end) {
|
||||
$key = $currentStart->format($format);
|
||||
$label = $currentStart->toAtomString();
|
||||
// normal entries
|
||||
$income['entries'][$label] = app('steam')->bcround(($currency[$key]['earned'] ?? '0'), $currency['currency_decimal_places']);
|
||||
$expense['entries'][$label] = app('steam')->bcround(($currency[$key]['spent'] ?? '0'), $currency['currency_decimal_places']);
|
||||
|
||||
// converted entries
|
||||
$income['native_entries'][$label] = app('steam')->bcround(($currency[$key]['native_earned'] ?? '0'), $currency['native_decimal_places']);
|
||||
$expense['native_entries'][$label] = app('steam')->bcround(($currency[$key]['native_spent'] ?? '0'), $currency['native_decimal_places']);
|
||||
|
||||
// next loop
|
||||
$currentStart = app('navigation')->addPeriod($currentStart, $preferredRange, 0);
|
||||
}
|
||||
|
||||
$chartData[] = $income;
|
||||
$chartData[] = $expense;
|
||||
}
|
||||
return response()->json($this->clean($chartData));
|
||||
}
|
||||
|
||||
}
|
313
app/Api/V2/Controllers/Chart/BudgetController.php
Normal file
313
app/Api/V2/Controllers/Chart/BudgetController.php
Normal file
@ -0,0 +1,313 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* BudgetController.php
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Api\V2\Controllers\Chart;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Api\V2\Controllers\Controller;
|
||||
use FireflyIII\Api\V2\Request\Generic\DateRequest;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Models\Budget;
|
||||
use FireflyIII\Models\BudgetLimit;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Repositories\Administration\Budget\BudgetRepositoryInterface;
|
||||
use FireflyIII\Repositories\Administration\Budget\OperationsRepositoryInterface;
|
||||
use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface;
|
||||
use FireflyIII\Support\Http\Api\CleansChartData;
|
||||
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class BudgetController
|
||||
*/
|
||||
class BudgetController extends Controller
|
||||
{
|
||||
use CleansChartData;
|
||||
|
||||
protected OperationsRepositoryInterface $opsRepository;
|
||||
private BudgetLimitRepositoryInterface $blRepository;
|
||||
private array $currencies = [];
|
||||
private TransactionCurrency $currency;
|
||||
private BudgetRepositoryInterface $repository;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->middleware(
|
||||
function ($request, $next) {
|
||||
$this->repository = app(BudgetRepositoryInterface::class);
|
||||
$this->blRepository = app(BudgetLimitRepositoryInterface::class);
|
||||
$this->opsRepository = app(OperationsRepositoryInterface::class);
|
||||
$this->currency = app('amount')->getDefaultCurrency();
|
||||
return $next($request);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateRequest $request
|
||||
*
|
||||
* TODO see autocomplete/accountcontroller
|
||||
*
|
||||
* @return JsonResponse
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function dashboard(DateRequest $request): JsonResponse
|
||||
{
|
||||
// get user.
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
// group ID
|
||||
$administrationId = $user->getAdministrationId();
|
||||
$this->repository->setAdministrationId($administrationId);
|
||||
$this->opsRepository->setAdministrationId($administrationId);
|
||||
|
||||
$params = $request->getAll();
|
||||
/** @var Carbon $start */
|
||||
$start = $params['start'];
|
||||
/** @var Carbon $end */
|
||||
$end = $params['end'];
|
||||
|
||||
// code from FrontpageChartGenerator, but not in separate class
|
||||
$budgets = $this->repository->getActiveBudgets();
|
||||
$data = [];
|
||||
/** @var Budget $budget */
|
||||
foreach ($budgets as $budget) {
|
||||
// could return multiple arrays, so merge.
|
||||
$data = array_merge($data, $this->processBudget($budget, $start, $end));
|
||||
}
|
||||
return response()->json($this->clean($data));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Budget $budget
|
||||
* @param Carbon $start
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return array
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function processBudget(Budget $budget, Carbon $start, Carbon $end): array
|
||||
{
|
||||
// get all limits:
|
||||
$limits = $this->blRepository->getBudgetLimits($budget, $start, $end);
|
||||
$rows = [];
|
||||
|
||||
// if no limits
|
||||
if (0 === $limits->count()) {
|
||||
// return as a single item in an array
|
||||
$rows = $this->noBudgetLimits($budget, $start, $end);
|
||||
}
|
||||
if ($limits->count() > 0) {
|
||||
$rows = $this->budgetLimits($budget, $limits);
|
||||
}
|
||||
// is always an array
|
||||
$return = [];
|
||||
foreach ($rows as $row) {
|
||||
$current = [
|
||||
'label' => $budget->name,
|
||||
'currency_id' => $row['currency_id'],
|
||||
'currency_code' => $row['currency_code'],
|
||||
'currency_name' => $row['currency_name'],
|
||||
'currency_decimal_places' => $row['currency_decimal_places'],
|
||||
'native_id' => $row['native_id'],
|
||||
'native_code' => $row['native_code'],
|
||||
'native_name' => $row['native_name'],
|
||||
'native_decimal_places' => $row['native_decimal_places'],
|
||||
'period' => null,
|
||||
'start' => $row['start'],
|
||||
'end' => $row['end'],
|
||||
'entries' => [
|
||||
'spent' => $row['spent'],
|
||||
'left' => $row['left'],
|
||||
'overspent' => $row['overspent'],
|
||||
],
|
||||
'native_entries' => [
|
||||
'spent' => $row['native_spent'],
|
||||
'left' => $row['native_left'],
|
||||
'overspent' => $row['native_overspent'],
|
||||
],
|
||||
];
|
||||
$return[] = $current;
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* When no budget limits are present, the expenses of the whole period are collected and grouped.
|
||||
* This is grouped per currency. Because there is no limit set, "left to spend" and "overspent" are empty.
|
||||
*
|
||||
* @param Budget $budget
|
||||
* @param Carbon $start
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return array
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function noBudgetLimits(Budget $budget, Carbon $start, Carbon $end): array
|
||||
{
|
||||
$budgetId = (int)$budget->id;
|
||||
$spent = $this->opsRepository->listExpenses($start, $end, null, new Collection([$budget]));
|
||||
return $this->processExpenses($budgetId, $spent, $start, $end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared between the "noBudgetLimits" function and "processLimit".
|
||||
*
|
||||
* Will take a single set of expenses and return its info.
|
||||
*
|
||||
* @param int $budgetId
|
||||
* @param array $array
|
||||
*
|
||||
* @return array
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function processExpenses(int $budgetId, array $array, Carbon $start, Carbon $end): array
|
||||
{
|
||||
$converter = new ExchangeRateConverter();
|
||||
$return = [];
|
||||
|
||||
/**
|
||||
* This array contains the expenses in this budget. Grouped per currency.
|
||||
* The grouping is on the main currency only.
|
||||
*
|
||||
* @var int $currencyId
|
||||
* @var array $block
|
||||
*/
|
||||
foreach ($array as $currencyId => $block) {
|
||||
$this->currencies[$currencyId] = $this->currencies[$currencyId] ?? TransactionCurrency::find($currencyId);
|
||||
$return[$currencyId] = $return[$currencyId] ?? [
|
||||
'currency_id' => $currencyId,
|
||||
'currency_code' => $block['currency_code'],
|
||||
'currency_name' => $block['currency_name'],
|
||||
'currency_symbol' => $block['currency_symbol'],
|
||||
'currency_decimal_places' => (int)$block['currency_decimal_places'],
|
||||
'native_id' => (int)$this->currency->id,
|
||||
'native_code' => $this->currency->code,
|
||||
'native_name' => $this->currency->name,
|
||||
'native_symbol' => $this->currency->symbol,
|
||||
'native_decimal_places' => (int)$this->currency->decimal_places,
|
||||
'start' => $start->toAtomString(),
|
||||
'end' => $end->toAtomString(),
|
||||
'spent' => '0',
|
||||
'native_spent' => '0',
|
||||
'left' => '0',
|
||||
'native_left' => '0',
|
||||
'overspent' => '0',
|
||||
'native_overspent' => '0',
|
||||
|
||||
];
|
||||
$currentBudgetArray = $block['budgets'][$budgetId];
|
||||
//var_dump($return);
|
||||
/** @var array $journal */
|
||||
foreach ($currentBudgetArray['transaction_journals'] as $journal) {
|
||||
|
||||
// convert the amount to the native currency.
|
||||
$rate = $converter->getCurrencyRate($this->currencies[$currencyId], $this->currency, $journal['date']);
|
||||
$convertedAmount = bcmul($journal['amount'], $rate);
|
||||
if ($journal['foreign_currency_id'] === $this->currency->id) {
|
||||
$convertedAmount = $journal['foreign_amount'];
|
||||
}
|
||||
|
||||
$return[$currencyId]['spent'] = bcadd($return[$currencyId]['spent'], $journal['amount']);
|
||||
$return[$currencyId]['native_spent'] = bcadd($return[$currencyId]['native_spent'], $convertedAmount);
|
||||
}
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that processes each budget limit (per budget).
|
||||
*
|
||||
* If you have a budget limit in EUR, only transactions in EUR will be considered.
|
||||
* If you have a budget limit in GBP, only transactions in GBP will be considered.
|
||||
*
|
||||
* If you have a budget limit in EUR, and a transaction in GBP, it will not be considered for the EUR budget limit.
|
||||
*
|
||||
* @param Budget $budget
|
||||
* @param Collection $limits
|
||||
*
|
||||
* @return array
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function budgetLimits(Budget $budget, Collection $limits): array
|
||||
{
|
||||
app('log')->debug(sprintf('Now in budgetLimits(#%d)', $budget->id));
|
||||
$data = [];
|
||||
/** @var BudgetLimit $limit */
|
||||
foreach ($limits as $limit) {
|
||||
$data = array_merge($data, $this->processLimit($budget, $limit));
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Budget $budget
|
||||
* @param BudgetLimit $limit
|
||||
*
|
||||
* @return array
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function processLimit(Budget $budget, BudgetLimit $limit): array
|
||||
{
|
||||
$budgetId = (int)$budget->id;
|
||||
$end = clone $limit->end_date;
|
||||
$end->endOfDay();
|
||||
$spent = $this->opsRepository->listExpenses($limit->start_date, $end, null, new Collection([$budget]));
|
||||
$limitCurrencyId = (int)$limit->transaction_currency_id;
|
||||
$limitCurrency = $limit->transactionCurrency;
|
||||
$converter = new ExchangeRateConverter();
|
||||
$filtered = [];
|
||||
$rate = $converter->getCurrencyRate($limitCurrency, $this->currency, $limit->start_date);
|
||||
$convertedLimitAmount = bcmul($limit->amount, $rate);
|
||||
|
||||
|
||||
/** @var array $entry */
|
||||
foreach ($spent as $currencyId => $entry) {
|
||||
// only spent the entry where the entry's currency matches the budget limit's currency
|
||||
// so $filtered will only have 1 or 0 entries
|
||||
if ($entry['currency_id'] === $limitCurrencyId) {
|
||||
$filtered[$currencyId] = $entry;
|
||||
}
|
||||
}
|
||||
$result = $this->processExpenses($budgetId, $filtered, $limit->start_date, $end);
|
||||
if (1 === count($result)) {
|
||||
$compare = bccomp((string)$limit->amount, app('steam')->positive($result[$limitCurrencyId]['spent']));
|
||||
if (1 === $compare) {
|
||||
// convert this amount into the native currency:
|
||||
$result[$limitCurrencyId]['left'] = bcadd($limit->amount, $result[$limitCurrencyId]['spent']);
|
||||
$result[$limitCurrencyId]['native_left'] = bcadd($convertedLimitAmount, $result[$limitCurrencyId]['native_spent']);
|
||||
}
|
||||
if ($compare <= 0) {
|
||||
$result[$limitCurrencyId]['overspent'] = app('steam')->positive(bcadd($limit->amount, $result[$limitCurrencyId]['spent']));
|
||||
$result[$limitCurrencyId]['native_overspent'] = app('steam')->positive(bcadd($convertedLimitAmount, $result[$limitCurrencyId]['native_spent']));
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
}
|
137
app/Api/V2/Controllers/Chart/CategoryController.php
Normal file
137
app/Api/V2/Controllers/Chart/CategoryController.php
Normal file
@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* BudgetController.php
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Api\V2\Controllers\Chart;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Api\V2\Controllers\Controller;
|
||||
use FireflyIII\Api\V2\Request\Generic\DateRequest;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
|
||||
use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use FireflyIII\Repositories\Administration\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
|
||||
use FireflyIII\Support\Http\Api\CleansChartData;
|
||||
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* Class BudgetController
|
||||
*/
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
use CleansChartData;
|
||||
|
||||
private AccountRepositoryInterface $accountRepos;
|
||||
private CurrencyRepositoryInterface $currencyRepos;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->middleware(
|
||||
function ($request, $next) {
|
||||
$this->accountRepos = app(AccountRepositoryInterface::class);
|
||||
$this->currencyRepos = app(CurrencyRepositoryInterface::class);
|
||||
$this->accountRepos->setAdministrationId(auth()->user()->user_group_id);
|
||||
return $next($request);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO may be worth to move to a handler but the data is simple enough.
|
||||
* TODO see autoComplete/account controller
|
||||
*
|
||||
* @param DateRequest $request
|
||||
*
|
||||
* @return JsonResponse
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function dashboard(DateRequest $request): JsonResponse
|
||||
{
|
||||
/** @var Carbon $start */
|
||||
$start = $this->parameters->get('start');
|
||||
/** @var Carbon $end */
|
||||
$end = $this->parameters->get('end');
|
||||
$accounts = $this->accountRepos->getAccountsByType([AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::ASSET, AccountType::DEFAULT]);
|
||||
$default = app('amount')->getDefaultCurrency();
|
||||
$converter = new ExchangeRateConverter();
|
||||
$currencies = [];
|
||||
$return = [];
|
||||
|
||||
// get journals for entire period:
|
||||
/** @var GroupCollectorInterface $collector */
|
||||
$collector = app(GroupCollectorInterface::class);
|
||||
$collector->setRange($start, $end)->withAccountInformation();
|
||||
$collector->setXorAccounts($accounts)->withCategoryInformation();
|
||||
$collector->setTypes([TransactionType::WITHDRAWAL, TransactionType::RECONCILIATION]);
|
||||
$journals = $collector->getExtractedJournals();
|
||||
|
||||
/** @var array $journal */
|
||||
foreach ($journals as $journal) {
|
||||
$currencyId = (int)$journal['currency_id'];
|
||||
$currency = $currencies[$currencyId] ?? $this->currencyRepos->find($currencyId);
|
||||
$currencies[$currencyId] = $currency;
|
||||
$categoryName = null === $journal['category_name'] ? (string)trans('firefly.no_category') : $journal['category_name'];
|
||||
$amount = app('steam')->positive($journal['amount']);
|
||||
$nativeAmount = $converter->convert($default, $currency, $journal['date'], $amount);
|
||||
$key = sprintf('%s-%s', $categoryName, $currency->code);
|
||||
if ((int)$journal['foreign_currency_id'] === (int)$default->id) {
|
||||
$nativeAmount = app('steam')->positive($journal['foreign_amount']);
|
||||
}
|
||||
// create arrays
|
||||
$return[$key] = $return[$key] ?? [
|
||||
'label' => $categoryName,
|
||||
'currency_id' => (int)$currency->id,
|
||||
'currency_code' => $currency->code,
|
||||
'currency_name' => $currency->name,
|
||||
'currency_symbol' => $currency->symbol,
|
||||
'currency_decimal_places' => (int)$currency->decimal_places,
|
||||
'native_id' => (int)$default->id,
|
||||
'native_code' => $default->code,
|
||||
'native_name' => $default->name,
|
||||
'native_symbol' => $default->symbol,
|
||||
'native_decimal_places' => (int)$default->decimal_places,
|
||||
'period' => null,
|
||||
'start' => $start->toAtomString(),
|
||||
'end' => $end->toAtomString(),
|
||||
'amount' => '0',
|
||||
'native_amount' => '0',
|
||||
];
|
||||
|
||||
|
||||
// add monies
|
||||
$return[$key]['amount'] = bcadd($return[$key]['amount'], $amount);
|
||||
$return[$key]['native_amount'] = bcadd($return[$key]['native_amount'], $nativeAmount);
|
||||
}
|
||||
$return = array_values($return);
|
||||
|
||||
// order by native amount
|
||||
usort($return, function (array $a, array $b) {
|
||||
return (float)$a['native_amount'] < (float)$b['native_amount'] ? 1 : -1;
|
||||
});
|
||||
return response()->json($this->clean($return));
|
||||
}
|
||||
|
||||
}
|
@ -93,21 +93,25 @@ class Controller extends BaseController
|
||||
// some date fields:
|
||||
foreach ($dates as $field) {
|
||||
$date = null;
|
||||
$obj = null;
|
||||
try {
|
||||
$date = request()->query->get($field);
|
||||
} catch (BadRequestException $e) {
|
||||
Log::error(sprintf('Request field "%s" contains a non-scalar value. Value set to NULL.', $field));
|
||||
Log::error($e->getMessage());
|
||||
$value = null;
|
||||
}
|
||||
$obj = null;
|
||||
if (null !== $date) {
|
||||
try {
|
||||
$obj = Carbon::parse($date);
|
||||
$obj = Carbon::parse($date, config('app.timezone'));
|
||||
} catch (InvalidDateException | InvalidFormatException $e) {
|
||||
// don't care
|
||||
app('log')->warning(sprintf('Ignored invalid date "%s" in API v2 controller parameter check: %s', substr($date, 0, 20), $e->getMessage()));
|
||||
}
|
||||
// out of range? set to null.
|
||||
if (null !== $obj && ($obj->year <= 1900 || $obj->year > 2099)) {
|
||||
app('log')->warning(sprintf('Refuse to use date "%s" in API v2 controller parameter check: %s', $field, $obj->toAtomString()));
|
||||
$obj = null;
|
||||
}
|
||||
}
|
||||
$bag->set($field, $obj);
|
||||
}
|
||||
@ -148,7 +152,7 @@ class Controller extends BaseController
|
||||
$objects = $paginator->getCollection();
|
||||
|
||||
// the transformer, at this point, needs to collect information that ALL items in the collection
|
||||
// require, like meta data and stuff like that, and save it for later.
|
||||
// require, like meta-data and stuff like that, and save it for later.
|
||||
$transformer->collectMetaData($objects);
|
||||
|
||||
$resource = new FractalCollection($objects, $transformer, $key);
|
||||
|
74
app/Api/V2/Controllers/Model/Bill/ShowController.php
Normal file
74
app/Api/V2/Controllers/Model/Bill/ShowController.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* ShowController.php
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Api\V2\Controllers\Model\Bill;
|
||||
|
||||
use FireflyIII\Api\V2\Controllers\Controller;
|
||||
use FireflyIII\Repositories\Administration\Bill\BillRepositoryInterface;
|
||||
use FireflyIII\Transformers\V2\BillTransformer;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
|
||||
/**
|
||||
* Class ShowController
|
||||
*/
|
||||
class ShowController extends Controller
|
||||
{
|
||||
private BillRepositoryInterface $repository;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->middleware(
|
||||
function ($request, $next) {
|
||||
$this->repository = app(BillRepositoryInterface::class);
|
||||
$this->repository->setAdministrationId(auth()->user()->user_group_id);
|
||||
return $next($request);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
*
|
||||
* TODO see autocomplete/accountcontroller for list.
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$this->repository->correctOrder();
|
||||
$bills = $this->repository->getBills();
|
||||
$pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data;
|
||||
$count = $bills->count();
|
||||
$bills = $bills->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
|
||||
$paginator = new LengthAwarePaginator($bills, $count, $pageSize, $this->parameters->get('page'));
|
||||
$transformer = new BillTransformer();
|
||||
$transformer->setParameters($this->parameters); // give params to transformer
|
||||
|
||||
return response()
|
||||
->json($this->jsonApiList('subscriptions', $paginator, $transformer))
|
||||
->header('Content-Type', self::CONTENT_TYPE);
|
||||
}
|
||||
}
|
@ -26,8 +26,7 @@ namespace FireflyIII\Api\V2\Controllers\Model\Bill;
|
||||
|
||||
use FireflyIII\Api\V2\Controllers\Controller;
|
||||
use FireflyIII\Api\V2\Request\Generic\DateRequest;
|
||||
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
|
||||
use FireflyIII\Support\Http\Api\ConvertsExchangeRates;
|
||||
use FireflyIII\Repositories\Administration\Bill\BillRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
@ -35,8 +34,6 @@ use Illuminate\Http\JsonResponse;
|
||||
*/
|
||||
class SumController extends Controller
|
||||
{
|
||||
use ConvertsExchangeRates;
|
||||
|
||||
private BillRepositoryInterface $repository;
|
||||
|
||||
/**
|
||||
@ -58,35 +55,37 @@ class SumController extends Controller
|
||||
* This endpoint is documented at:
|
||||
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v2)#/transactions-sum/getBillsPaidTrSum
|
||||
*
|
||||
* TODO see autocomplete/accountcontroller for list.
|
||||
*
|
||||
* @param DateRequest $request
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function paid(DateRequest $request): JsonResponse
|
||||
{
|
||||
$dates = $request->getAll();
|
||||
$result = $this->repository->sumPaidInRange($dates['start'], $dates['end']);
|
||||
$converted = $this->cerSum($result);
|
||||
$this->repository->setAdministrationId(auth()->user()->user_group_id);
|
||||
$result = $this->repository->sumPaidInRange($this->parameters->get('start'), $this->parameters->get('end'));
|
||||
|
||||
// convert to JSON response:
|
||||
return response()->api($converted);
|
||||
return response()->api(array_values($result));
|
||||
}
|
||||
|
||||
/**
|
||||
* This endpoint is documented at:
|
||||
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v2)#/transactions-sum/getBillsUnpaidTrSum
|
||||
*
|
||||
* TODO see autocomplete/accountcontroller for list.
|
||||
*
|
||||
* @param DateRequest $request
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function unpaid(DateRequest $request): JsonResponse
|
||||
{
|
||||
$dates = $request->getAll();
|
||||
$result = $this->repository->sumUnpaidInRange($dates['start'], $dates['end']);
|
||||
$converted = $this->cerSum($result);
|
||||
$this->repository->setAdministrationId(auth()->user()->user_group_id);
|
||||
$result = $this->repository->sumUnpaidInRange($this->parameters->get('start'), $this->parameters->get('end'));
|
||||
|
||||
// convert to JSON response:
|
||||
return response()->api($converted);
|
||||
return response()->api(array_values($result));
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +60,8 @@ class ListController extends Controller
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
echo 'this needs move to Administration';
|
||||
exit;
|
||||
$collection = $this->repository->getActiveBudgets();
|
||||
$total = $collection->count();
|
||||
$collection->slice($this->pageSize * $this->parameters->get('page'), $this->pageSize);
|
||||
|
@ -29,7 +29,6 @@ use FireflyIII\Api\V2\Controllers\Controller;
|
||||
use FireflyIII\Api\V2\Request\Generic\DateRequest;
|
||||
use FireflyIII\Models\Budget;
|
||||
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
|
||||
use FireflyIII\Support\Http\Api\ConvertsExchangeRates;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
@ -37,8 +36,6 @@ use Illuminate\Http\JsonResponse;
|
||||
*/
|
||||
class ShowController extends Controller
|
||||
{
|
||||
use ConvertsExchangeRates;
|
||||
|
||||
private BudgetRepositoryInterface $repository;
|
||||
|
||||
/**
|
||||
@ -63,6 +60,7 @@ class ShowController extends Controller
|
||||
*/
|
||||
public function budgeted(DateRequest $request, Budget $budget): JsonResponse
|
||||
{
|
||||
die('deprecated use of thing.');
|
||||
$data = $request->getAll();
|
||||
$result = $this->repository->budgetedInPeriodForBudget($budget, $data['start'], $data['end']);
|
||||
$converted = $this->cerSum(array_values($result));
|
||||
@ -77,6 +75,7 @@ class ShowController extends Controller
|
||||
*/
|
||||
public function spent(DateRequest $request, Budget $budget): JsonResponse
|
||||
{
|
||||
die('deprecated use of thing.');
|
||||
$data = $request->getAll();
|
||||
$result = $this->repository->spentInPeriodForBudget($budget, $data['start'], $data['end']);
|
||||
$converted = $this->cerSum(array_values($result));
|
||||
|
@ -27,7 +27,6 @@ namespace FireflyIII\Api\V2\Controllers\Model\Budget;
|
||||
use FireflyIII\Api\V2\Controllers\Controller;
|
||||
use FireflyIII\Api\V2\Request\Generic\DateRequest;
|
||||
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
|
||||
use FireflyIII\Support\Http\Api\ConvertsExchangeRates;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
@ -35,8 +34,6 @@ use Illuminate\Http\JsonResponse;
|
||||
*/
|
||||
class SumController extends Controller
|
||||
{
|
||||
use ConvertsExchangeRates;
|
||||
|
||||
private BudgetRepositoryInterface $repository;
|
||||
|
||||
/**
|
||||
@ -64,6 +61,7 @@ class SumController extends Controller
|
||||
*/
|
||||
public function budgeted(DateRequest $request): JsonResponse
|
||||
{
|
||||
die('deprecated use of thing.');
|
||||
$data = $request->getAll();
|
||||
$result = $this->repository->budgetedInPeriod($data['start'], $data['end']);
|
||||
$converted = $this->cerSum(array_values($result));
|
||||
@ -81,6 +79,7 @@ class SumController extends Controller
|
||||
*/
|
||||
public function spent(DateRequest $request): JsonResponse
|
||||
{
|
||||
die('deprecated use of thing.');
|
||||
$data = $request->getAll();
|
||||
$result = $this->repository->spentInPeriod($data['start'], $data['end']);
|
||||
$converted = $this->cerSum(array_values($result));
|
||||
|
73
app/Api/V2/Controllers/Model/PiggyBank/ShowController.php
Normal file
73
app/Api/V2/Controllers/Model/PiggyBank/ShowController.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* ShowController.php
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Api\V2\Controllers\Model\PiggyBank;
|
||||
|
||||
use FireflyIII\Api\V2\Controllers\Controller;
|
||||
use FireflyIII\Repositories\Administration\PiggyBank\PiggyBankRepositoryInterface;
|
||||
use FireflyIII\Transformers\V2\PiggyBankTransformer;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
|
||||
/**
|
||||
* Class ShowController
|
||||
*/
|
||||
class ShowController extends Controller
|
||||
{
|
||||
private PiggyBankRepositoryInterface $repository;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->middleware(
|
||||
function ($request, $next) {
|
||||
$this->repository = app(PiggyBankRepositoryInterface::class);
|
||||
$this->repository->setAdministrationId(auth()->user()->user_group_id);
|
||||
return $next($request);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
*
|
||||
* TODO see autocomplete/accountcontroller for list.
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$piggies = $this->repository->getPiggyBanks();
|
||||
$pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data;
|
||||
$count = $piggies->count();
|
||||
$piggies = $piggies->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
|
||||
$paginator = new LengthAwarePaginator($piggies, $count, $pageSize, $this->parameters->get('page'));
|
||||
$transformer = new PiggyBankTransformer();
|
||||
$transformer->setParameters($this->parameters); // give params to transformer
|
||||
|
||||
return response()
|
||||
->json($this->jsonApiList('piggy-banks', $paginator, $transformer))
|
||||
->header('Content-Type', self::CONTENT_TYPE);
|
||||
}
|
||||
}
|
@ -26,7 +26,6 @@ namespace FireflyIII\Api\V2\Controllers;
|
||||
|
||||
use FireflyIII\Api\V2\Request\Generic\SingleDateRequest;
|
||||
use FireflyIII\Helpers\Report\NetWorthInterface;
|
||||
use FireflyIII\Support\Http\Api\ConvertsExchangeRates;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
@ -34,8 +33,6 @@ use Illuminate\Http\JsonResponse;
|
||||
*/
|
||||
class NetWorthController extends Controller
|
||||
{
|
||||
use ConvertsExchangeRates;
|
||||
|
||||
private NetWorthInterface $netWorth;
|
||||
|
||||
/**
|
||||
@ -64,6 +61,7 @@ class NetWorthController extends Controller
|
||||
*/
|
||||
public function get(SingleDateRequest $request): JsonResponse
|
||||
{
|
||||
die('deprecated use of thing.');
|
||||
$date = $request->getDate();
|
||||
$result = $this->netWorth->sumNetWorthByCurrency($date);
|
||||
$converted = $this->cerSum($result);
|
||||
|
543
app/Api/V2/Controllers/Summary/BasicController.php
Normal file
543
app/Api/V2/Controllers/Summary/BasicController.php
Normal file
@ -0,0 +1,543 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* SummaryController.php
|
||||
* Copyright (c) 2021 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Api\V2\Controllers\Summary;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
use FireflyIII\Api\V2\Controllers\Controller;
|
||||
use FireflyIII\Api\V2\Request\Generic\DateRequest;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
|
||||
use FireflyIII\Helpers\Report\NetWorthInterface;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use FireflyIII\Models\UserGroup;
|
||||
use FireflyIII\Repositories\Administration\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Repositories\Administration\Bill\BillRepositoryInterface;
|
||||
use FireflyIII\Repositories\Administration\Budget\AvailableBudgetRepositoryInterface;
|
||||
use FireflyIII\Repositories\Administration\Budget\BudgetRepositoryInterface;
|
||||
use FireflyIII\Repositories\Administration\Budget\OperationsRepositoryInterface;
|
||||
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
|
||||
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* Class BasicController
|
||||
*/
|
||||
class BasicController extends Controller
|
||||
{
|
||||
private AvailableBudgetRepositoryInterface $abRepository;
|
||||
private AccountRepositoryInterface $accountRepository;
|
||||
private BillRepositoryInterface $billRepository;
|
||||
private BudgetRepositoryInterface $budgetRepository;
|
||||
private CurrencyRepositoryInterface $currencyRepos;
|
||||
private OperationsRepositoryInterface $opsRepository;
|
||||
|
||||
/**
|
||||
* BasicController constructor.
|
||||
*
|
||||
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->middleware(
|
||||
function ($request, $next) {
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
$this->abRepository = app(AvailableBudgetRepositoryInterface::class);
|
||||
$this->accountRepository = app(AccountRepositoryInterface::class);
|
||||
$this->billRepository = app(BillRepositoryInterface::class);
|
||||
$this->budgetRepository = app(BudgetRepositoryInterface::class);
|
||||
$this->currencyRepos = app(CurrencyRepositoryInterface::class);
|
||||
$this->opsRepository = app(OperationsRepositoryInterface::class);
|
||||
|
||||
$this->abRepository->setAdministrationId($user->user_group_id);
|
||||
$this->accountRepository->setAdministrationId($user->user_group_id);
|
||||
$this->billRepository->setAdministrationId($user->user_group_id);
|
||||
$this->budgetRepository->setAdministrationId($user->user_group_id);
|
||||
$this->currencyRepos->setUser($user);
|
||||
$this->opsRepository->setAdministrationId($user->user_group_id);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This endpoint is documented at:
|
||||
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v2)#/summary/getBasicSummary
|
||||
*
|
||||
* @param DateRequest $request
|
||||
*
|
||||
* @return JsonResponse
|
||||
* @throws Exception
|
||||
*/
|
||||
public function basic(DateRequest $request): JsonResponse
|
||||
{
|
||||
// parameters for boxes:
|
||||
$start = $this->parameters->get('start');
|
||||
$end = $this->parameters->get('end');
|
||||
|
||||
// balance information:
|
||||
$balanceData = [];
|
||||
$billData = [];
|
||||
$spentData = [];
|
||||
$netWorthData = [];
|
||||
$balanceData = $this->getBalanceInformation($start, $end);
|
||||
$billData = $this->getBillInformation($start, $end);
|
||||
$spentData = $this->getLeftToSpendInfo($start, $end);
|
||||
$netWorthData = $this->getNetWorthInfo($start, $end);
|
||||
$total = array_merge($balanceData, $billData, $spentData, $netWorthData);
|
||||
return response()->json($total);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Carbon $start
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return array
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function getBalanceInformation(Carbon $start, Carbon $end): array
|
||||
{
|
||||
// prep some arrays:
|
||||
$incomes = [];
|
||||
$expenses = [];
|
||||
$sums = [];
|
||||
$return = [];
|
||||
$currencies = [];
|
||||
$converter = new ExchangeRateConverter();
|
||||
$default = app('amount')->getDefaultCurrency();
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
// collect income of user using the new group collector.
|
||||
/** @var GroupCollectorInterface $collector */
|
||||
$collector = app(GroupCollectorInterface::class);
|
||||
$collector
|
||||
->setRange($start, $end)
|
||||
->setUserGroup($user->userGroup)
|
||||
// set page to retrieve
|
||||
->setPage($this->parameters->get('page'))
|
||||
// set types of transactions to return.
|
||||
->setTypes([TransactionType::DEPOSIT])
|
||||
->setRange($start, $end);
|
||||
|
||||
$set = $collector->getExtractedJournals();
|
||||
/** @var array $transactionJournal */
|
||||
foreach ($set as $transactionJournal) {
|
||||
// transaction info:
|
||||
$currencyId = (int)$transactionJournal['currency_id'];
|
||||
$amount = bcmul($transactionJournal['amount'], '-1');
|
||||
$currency = $currencies[$currencyId] ?? TransactionCurrency::find($currencyId);
|
||||
$currencies[$currencyId] = $currency;
|
||||
$nativeAmount = $converter->convert($currency, $default, $transactionJournal['date'], $amount);
|
||||
if ((int)$transactionJournal['foreign_currency_id'] === (int)$default->id) {
|
||||
// use foreign amount instead
|
||||
$nativeAmount = $transactionJournal['foreign_amount'];
|
||||
}
|
||||
// prep the arrays
|
||||
$incomes[$currencyId] = $incomes[$currencyId] ?? '0';
|
||||
$incomes['native'] = $incomes['native'] ?? '0';
|
||||
$sums[$currencyId] = $sums[$currencyId] ?? '0';
|
||||
$sums['native'] = $sums['native'] ?? '0';
|
||||
|
||||
// add values:
|
||||
$incomes[$currencyId] = bcadd($incomes[$currencyId], $amount);
|
||||
$sums[$currencyId] = bcadd($sums[$currencyId], $amount);
|
||||
$incomes['native'] = bcadd($incomes['native'], $nativeAmount);
|
||||
$sums['native'] = bcadd($sums['native'], $nativeAmount);
|
||||
}
|
||||
|
||||
// collect expenses of user using the new group collector.
|
||||
/** @var GroupCollectorInterface $collector */
|
||||
$collector = app(GroupCollectorInterface::class);
|
||||
$collector
|
||||
->setRange($start, $end)
|
||||
->setUserGroup($user->userGroup)
|
||||
// set page to retrieve
|
||||
->setPage($this->parameters->get('page'))
|
||||
// set types of transactions to return.
|
||||
->setTypes([TransactionType::WITHDRAWAL])
|
||||
->setRange($start, $end);
|
||||
$set = $collector->getExtractedJournals();
|
||||
|
||||
/** @var array $transactionJournal */
|
||||
foreach ($set as $transactionJournal) {
|
||||
// transaction info
|
||||
$currencyId = (int)$transactionJournal['currency_id'];
|
||||
$amount = $transactionJournal['amount'];
|
||||
$currency = $currencies[$currencyId] ?? $this->currencyRepos->find($currencyId);
|
||||
$currencies[$currencyId] = $currency;
|
||||
$nativeAmount = $converter->convert($currency, $default, $transactionJournal['date'], $amount);
|
||||
if ((int)$transactionJournal['foreign_currency_id'] === (int)$default->id) {
|
||||
// use foreign amount instead
|
||||
$nativeAmount = $transactionJournal['foreign_amount'];
|
||||
}
|
||||
|
||||
// prep arrays
|
||||
$expenses[$currencyId] = $expenses[$currencyId] ?? '0';
|
||||
$expenses['native'] = $expenses['native'] ?? '0';
|
||||
$sums[$currencyId] = $sums[$currencyId] ?? '0';
|
||||
$sums['native'] = $sums['native'] ?? '0';
|
||||
|
||||
// add values
|
||||
$expenses[$currencyId] = bcadd($expenses[$currencyId], $amount);
|
||||
$sums[$currencyId] = bcadd($sums[$currencyId], $amount);
|
||||
$expenses['native'] = bcadd($expenses['native'], $nativeAmount);
|
||||
$sums['native'] = bcadd($sums['native'], $nativeAmount);
|
||||
}
|
||||
|
||||
// create special array for native currency:
|
||||
$return[] = [
|
||||
'key' => 'balance-in-native',
|
||||
'value' => $sums['native'],
|
||||
'currency_id' => $default->id,
|
||||
'currency_code' => $default->code,
|
||||
'currency_symbol' => $default->symbol,
|
||||
'currency_decimal_places' => $default->decimal_places,
|
||||
];
|
||||
$return[] = [
|
||||
'key' => 'spent-in-native',
|
||||
'value' => $expenses['native'],
|
||||
'currency_id' => $default->id,
|
||||
'currency_code' => $default->code,
|
||||
'currency_symbol' => $default->symbol,
|
||||
'currency_decimal_places' => $default->decimal_places,
|
||||
];
|
||||
$return[] = [
|
||||
'key' => 'earned-in-native',
|
||||
'value' => $incomes['native'],
|
||||
'currency_id' => $default->id,
|
||||
'currency_code' => $default->code,
|
||||
'currency_symbol' => $default->symbol,
|
||||
'currency_decimal_places' => $default->decimal_places,
|
||||
];
|
||||
|
||||
// format amounts:
|
||||
$keys = array_keys($sums);
|
||||
foreach ($keys as $currencyId) {
|
||||
if ('native' === $currencyId) {
|
||||
// skip native entries.
|
||||
continue;
|
||||
}
|
||||
$currency = $currencies[$currencyId] ?? $this->currencyRepos->find($currencyId);
|
||||
$currencies[$currencyId] = $currency;
|
||||
// create objects for big array.
|
||||
$return[] = [
|
||||
'key' => sprintf('balance-in-%s', $currency->code),
|
||||
'value' => $sums[$currencyId] ?? '0',
|
||||
'currency_id' => $currency->id,
|
||||
'currency_code' => $currency->code,
|
||||
'currency_symbol' => $currency->symbol,
|
||||
'currency_decimal_places' => $currency->decimal_places,
|
||||
];
|
||||
$return[] = [
|
||||
'key' => sprintf('spent-in-%s', $currency->code),
|
||||
'value' => $expenses[$currencyId] ?? '0',
|
||||
'currency_id' => $currency->id,
|
||||
'currency_code' => $currency->code,
|
||||
'currency_symbol' => $currency->symbol,
|
||||
'currency_decimal_places' => $currency->decimal_places,
|
||||
];
|
||||
$return[] = [
|
||||
'key' => sprintf('earned-in-%s', $currency->code),
|
||||
'value' => $incomes[$currencyId] ?? '0',
|
||||
'currency_id' => $currency->id,
|
||||
'currency_code' => $currency->code,
|
||||
'currency_symbol' => $currency->symbol,
|
||||
'currency_decimal_places' => $currency->decimal_places,
|
||||
];
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Carbon $start
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getBillInformation(Carbon $start, Carbon $end): array
|
||||
{
|
||||
/*
|
||||
* Since both this method and the chart use the exact same data, we can suffice
|
||||
* with calling the one method in the bill repository that will get this amount.
|
||||
*/
|
||||
$paidAmount = $this->billRepository->sumPaidInRange($start, $end);
|
||||
$unpaidAmount = $this->billRepository->sumUnpaidInRange($start, $end);
|
||||
|
||||
$return = [];
|
||||
/**
|
||||
* @var array $info
|
||||
*/
|
||||
foreach ($paidAmount as $info) {
|
||||
$amount = bcmul($info['sum'], '-1');
|
||||
$nativeAmount = bcmul($info['native_sum'], '-1');
|
||||
$return[] = [
|
||||
'key' => sprintf('bills-paid-in-%s', $info['currency_code']),
|
||||
'value' => $amount,
|
||||
'currency_id' => $info['currency_id'],
|
||||
'currency_code' => $info['currency_code'],
|
||||
'currency_symbol' => $info['currency_symbol'],
|
||||
'currency_decimal_places' => $info['currency_decimal_places'],
|
||||
];
|
||||
$return[] = [
|
||||
'key' => 'bills-paid-in-native',
|
||||
'value' => $nativeAmount,
|
||||
'currency_id' => $info['native_id'],
|
||||
'currency_code' => $info['native_code'],
|
||||
'currency_symbol' => $info['native_symbol'],
|
||||
'currency_decimal_places' => $info['native_decimal_places'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array $info
|
||||
*/
|
||||
foreach ($unpaidAmount as $info) {
|
||||
$amount = bcmul($info['sum'], '-1');
|
||||
$nativeAmount = bcmul($info['native_sum'], '-1');
|
||||
$return[] = [
|
||||
'key' => sprintf('bills-unpaid-in-%s', $info['currency_code']),
|
||||
'value' => $amount,
|
||||
'currency_id' => $info['currency_id'],
|
||||
'currency_code' => $info['currency_code'],
|
||||
'currency_symbol' => $info['currency_symbol'],
|
||||
'currency_decimal_places' => $info['currency_decimal_places'],
|
||||
];
|
||||
$return[] = [
|
||||
'key' => 'bills-unpaid-in-native',
|
||||
'value' => $nativeAmount,
|
||||
'currency_id' => $info['native_id'],
|
||||
'currency_code' => $info['native_code'],
|
||||
'currency_symbol' => $info['native_symbol'],
|
||||
'currency_decimal_places' => $info['native_decimal_places'],
|
||||
];
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Carbon $start
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
private function getLeftToSpendInfo(Carbon $start, Carbon $end): array
|
||||
{
|
||||
$return = [];
|
||||
$today = today(config('app.timezone'));
|
||||
$available = $this->abRepository->getAvailableBudgetWithCurrency($start, $end);
|
||||
$budgets = $this->budgetRepository->getActiveBudgets();
|
||||
$spent = $this->opsRepository->listExpenses($start, $end, null, $budgets);
|
||||
$default = app('amount')->getDefaultCurrency();
|
||||
$currencies = [];
|
||||
$converter = new ExchangeRateConverter();
|
||||
|
||||
// native info:
|
||||
$nativeLeft = [
|
||||
'key' => 'left-to-spend-in-native',
|
||||
'value' => '0',
|
||||
'currency_id' => (int)$default->id,
|
||||
'currency_code' => $default->code,
|
||||
'currency_symbol' => $default->symbol,
|
||||
'currency_decimal_places' => (int)$default->decimal_places,
|
||||
];
|
||||
$nativePerDay = [
|
||||
'key' => 'left-per-day-to-spend-in-native',
|
||||
'value' => '0',
|
||||
'currency_id' => (int)$default->id,
|
||||
'currency_code' => $default->code,
|
||||
'currency_symbol' => $default->symbol,
|
||||
'currency_decimal_places' => (int)$default->decimal_places,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var int $currencyId
|
||||
* @var array $row
|
||||
*/
|
||||
foreach ($spent as $currencyId => $row) {
|
||||
$spent = '0';
|
||||
$spentNative = '0';
|
||||
// get the sum from the array of transactions (double loop but who cares)
|
||||
/** @var array $budget */
|
||||
foreach ($row['budgets'] as $budget) {
|
||||
/** @var array $journal */
|
||||
foreach ($budget['transaction_journals'] as $journal) {
|
||||
$journalCurrencyId = $journal['currency_id'];
|
||||
$currency = $currencies[$journalCurrencyId] ?? $this->currencyRepos->find($journalCurrencyId);
|
||||
$currencies[$currencyId] = $currency;
|
||||
$amount = bcmul($journal['amount'], '-1');
|
||||
$amountNative = $converter->convert($default, $currency, $start, $amount);
|
||||
if ((int)$journal['foreign_currency_id'] === (int)$default->id) {
|
||||
$amountNative = $journal['foreign_amount'];
|
||||
}
|
||||
$spent = bcadd($spent, $amount);
|
||||
$spentNative = bcadd($spentNative, $amountNative);
|
||||
}
|
||||
}
|
||||
|
||||
// either an amount was budgeted or 0 is available.
|
||||
$currency = $currencies[$currencyId] ?? $this->currencyRepos->find($currencyId);
|
||||
$currencies[$currencyId] = $currency;
|
||||
$amount = $available[$currencyId]['amount'] ?? '0';
|
||||
$amountNative = $converter->convert($default, $currency, $start, $amount);
|
||||
$left = bcadd($amount, $spent);
|
||||
$leftNative = bcadd($amountNative, $spentNative);
|
||||
|
||||
// how much left per day?
|
||||
$days = $today->diffInDays($end) + 1;
|
||||
$perDay = '0';
|
||||
$perDayNative = '0';
|
||||
if (0 !== $days && bccomp($left, '0') > -1) {
|
||||
$perDay = bcdiv($left, (string)$days);
|
||||
}
|
||||
if (0 !== $days && bccomp($leftNative, '0') > -1) {
|
||||
$perDayNative = bcdiv($leftNative, (string)$days);
|
||||
}
|
||||
|
||||
// left
|
||||
$return[] = [
|
||||
'key' => sprintf('left-to-spend-in-%s', $row['currency_code']),
|
||||
'value' => $left,
|
||||
'currency_id' => $row['currency_id'],
|
||||
'currency_code' => $row['currency_code'],
|
||||
'currency_symbol' => $row['currency_symbol'],
|
||||
'currency_decimal_places' => $row['currency_decimal_places'],
|
||||
];
|
||||
// left (native)
|
||||
$nativeLeft['value'] = $leftNative;
|
||||
|
||||
// left per day:
|
||||
$return[] = [
|
||||
'key' => sprintf('left-per-day-to-spend-in-%s', $row['currency_code']),
|
||||
'value' => $perDay,
|
||||
'currency_id' => $row['currency_id'],
|
||||
'currency_code' => $row['currency_code'],
|
||||
'currency_symbol' => $row['currency_symbol'],
|
||||
'currency_decimal_places' => $row['currency_decimal_places'],
|
||||
];
|
||||
|
||||
// left per day (native)
|
||||
$nativePerDay['value'] = $perDayNative;
|
||||
}
|
||||
$return[] = $nativeLeft;
|
||||
$return[] = $nativePerDay;
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Carbon $start
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getNetWorthInfo(Carbon $start, Carbon $end): array
|
||||
{
|
||||
/** @var UserGroup $userGroup */
|
||||
$userGroup = auth()->user()->userGroup;
|
||||
$date = today(config('app.timezone'))->startOfDay();
|
||||
// start and end in the future? use $end
|
||||
if ($this->notInDateRange($date, $start, $end)) {
|
||||
/** @var Carbon $date */
|
||||
$date = session('end', today(config('app.timezone'))->endOfMonth());
|
||||
}
|
||||
|
||||
/** @var NetWorthInterface $netWorthHelper */
|
||||
$netWorthHelper = app(NetWorthInterface::class);
|
||||
$netWorthHelper->setUserGroup($userGroup);
|
||||
$allAccounts = $this->accountRepository->getActiveAccountsByType(
|
||||
[AccountType::ASSET, AccountType::DEFAULT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::DEBT]
|
||||
);
|
||||
|
||||
// filter list on preference of being included.
|
||||
$filtered = $allAccounts->filter(
|
||||
function (Account $account) {
|
||||
$includeNetWorth = $this->accountRepository->getMetaValue($account, 'include_net_worth');
|
||||
|
||||
return null === $includeNetWorth || '1' === $includeNetWorth;
|
||||
}
|
||||
);
|
||||
|
||||
$netWorthSet = $netWorthHelper->byAccounts($filtered, $date);
|
||||
$return = [];
|
||||
// in native amount
|
||||
$return[] = [
|
||||
'key' => 'net-worth-in-native',
|
||||
'value' => $netWorthSet['native']['balance'],
|
||||
'currency_id' => $netWorthSet['native']['currency_id'],
|
||||
'currency_code' => $netWorthSet['native']['currency_code'],
|
||||
'currency_symbol' => $netWorthSet['native']['currency_symbol'],
|
||||
'currency_decimal_places' => $netWorthSet['native']['currency_decimal_places'],
|
||||
];
|
||||
foreach ($netWorthSet as $key => $data) {
|
||||
if ('native' === $key) {
|
||||
continue;
|
||||
}
|
||||
$return[] = [
|
||||
'key' => sprintf('net-worth-in-%s', $data['currency_code']),
|
||||
'value' => $data['balance'],
|
||||
'currency_id' => $data['currency_id'],
|
||||
'currency_code' => $data['currency_code'],
|
||||
'currency_symbol' => $data['currency_symbol'],
|
||||
'currency_decimal_places' => $data['currency_decimal_places'],
|
||||
];
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if date is outside session range.
|
||||
*
|
||||
* @param Carbon $date
|
||||
*
|
||||
* @param Carbon $start
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function notInDateRange(Carbon $date, Carbon $start, Carbon $end): bool // Validate a preference
|
||||
{
|
||||
$result = false;
|
||||
if ($start->greaterThanOrEqualTo($date) && $end->greaterThanOrEqualTo($date)) {
|
||||
$result = true;
|
||||
}
|
||||
// start and end in the past? use $end
|
||||
if ($start->lessThanOrEqualTo($date) && $end->lessThanOrEqualTo($date)) {
|
||||
$result = true;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
@ -49,19 +49,17 @@ class AccountController extends Controller
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function listTransactions(ListRequest $request, Account $account): JsonResponse
|
||||
public function list(ListRequest $request, Account $account): JsonResponse
|
||||
{
|
||||
// collect transactions:
|
||||
$type = $request->get('type') ?? 'default';
|
||||
$limit = (int)$request->get('limit');
|
||||
$page = (int)$request->get('page');
|
||||
$limit = $request->getLimit();
|
||||
$page = $request->getPage();
|
||||
$page = max($page, 1);
|
||||
|
||||
if ($limit > 0 && $limit <= $this->pageSize) {
|
||||
$this->pageSize = $limit;
|
||||
}
|
||||
|
||||
$types = $this->mapTransactionTypes($type);
|
||||
|
||||
/** @var GroupCollectorInterface $collector */
|
||||
$collector = app(GroupCollectorInterface::class);
|
||||
@ -69,15 +67,25 @@ class AccountController extends Controller
|
||||
->withAPIInformation()
|
||||
->setLimit($this->pageSize)
|
||||
->setPage($page)
|
||||
->setTypes($types);
|
||||
->setTypes($request->getTransactionTypes());
|
||||
|
||||
// TODO date filter
|
||||
//if (null !== $this->parameters->get('start') && null !== $this->parameters->get('end')) {
|
||||
// $collector->setRange($this->parameters->get('start'), $this->parameters->get('end'));
|
||||
//}
|
||||
$start = $request->getStartDate();
|
||||
$end = $request->getEndDate();
|
||||
if (null !== $start) {
|
||||
$collector->setStart($start);
|
||||
}
|
||||
if (null !== $end) {
|
||||
$collector->setEnd($start);
|
||||
}
|
||||
|
||||
$paginator = $collector->getPaginatedGroups();
|
||||
$paginator->setPath(route('api.v2.accounts.transactions', [$account->id])); // TODO . $this->buildParams()
|
||||
$paginator->setPath(
|
||||
sprintf(
|
||||
'%s?%s',
|
||||
route('api.v2.accounts.transactions', [$account->id]),
|
||||
$request->buildParams()
|
||||
)
|
||||
);
|
||||
|
||||
return response()
|
||||
->json($this->jsonApiList('transactions', $paginator, new TransactionGroupTransformer()))
|
||||
|
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* TransactionController.php
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Api\V2\Controllers\Transaction\List;
|
||||
|
||||
use FireflyIII\Api\V2\Controllers\Controller;
|
||||
use FireflyIII\Api\V2\Request\Transaction\ListRequest;
|
||||
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
|
||||
use FireflyIII\Transformers\V2\TransactionGroupTransformer;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* Class TransactionController
|
||||
*/
|
||||
class TransactionController extends Controller
|
||||
{
|
||||
/**
|
||||
* @param ListRequest $request
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function list(ListRequest $request): JsonResponse
|
||||
{
|
||||
// collect transactions:
|
||||
$limit = $request->getLimit();
|
||||
$page = $request->getPage();
|
||||
$page = max($page, 1);
|
||||
|
||||
if ($limit > 0 && $limit <= $this->pageSize) {
|
||||
$this->pageSize = $limit;
|
||||
}
|
||||
|
||||
|
||||
/** @var GroupCollectorInterface $collector */
|
||||
$collector = app(GroupCollectorInterface::class);
|
||||
$collector->setUserGroup(auth()->user()->userGroup)
|
||||
->withAPIInformation()
|
||||
->setLimit($this->pageSize)
|
||||
->setPage($page)
|
||||
->setTypes($request->getTransactionTypes());
|
||||
|
||||
$start = $this->parameters->get('start');
|
||||
$end = $this->parameters->get('end');
|
||||
if (null !== $start) {
|
||||
$collector->setStart($start);
|
||||
}
|
||||
if (null !== $end) {
|
||||
$collector->setEnd($end);
|
||||
}
|
||||
|
||||
// $collector->dumpQuery();
|
||||
// exit;
|
||||
|
||||
$paginator = $collector->getPaginatedGroups();
|
||||
$paginator->setPath(
|
||||
sprintf(
|
||||
'%s?%s',
|
||||
route('api.v2.transactions.list'),
|
||||
$request->buildParams()
|
||||
)
|
||||
);
|
||||
|
||||
return response()
|
||||
->json($this->jsonApiList('transactions', $paginator, new TransactionGroupTransformer()))
|
||||
->header('Content-Type', self::CONTENT_TYPE);
|
||||
}
|
||||
|
||||
|
||||
}
|
85
app/Api/V2/Request/Chart/BalanceChartRequest.php
Normal file
85
app/Api/V2/Request/Chart/BalanceChartRequest.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* BalanceChartRequest.php
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Api\V2\Request\Chart;
|
||||
|
||||
use FireflyIII\Support\Request\ChecksLogin;
|
||||
use FireflyIII\Support\Request\ConvertsDataTypes;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class BalanceChartRequest extends FormRequest
|
||||
{
|
||||
use ConvertsDataTypes;
|
||||
use ChecksLogin;
|
||||
|
||||
/**
|
||||
* Get all data from the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAll(): array
|
||||
{
|
||||
return [
|
||||
'accounts' => $this->getAccountList(),
|
||||
'period' => $this->string('period'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The rules that the incoming request must be matched against.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'start' => 'required|date|after:1900-01-01|before:2099-12-31',
|
||||
'end' => 'required|date|after_or_equal:start|before:2099-12-31|after:1900-01-01',
|
||||
'accounts.*' => 'required|exists:accounts,id',
|
||||
'period' => sprintf('required|in:%s', join(',', config('firefly.valid_view_ranges'))),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Validator $validator
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function withValidator(Validator $validator): void
|
||||
{
|
||||
$validator->after(
|
||||
function (Validator $validator) {
|
||||
// validate transaction query data.
|
||||
$data = $validator->getData();
|
||||
if (!array_key_exists('accounts', $data)) {
|
||||
$validator->errors()->add('accounts', trans('validation.filled', ['attribute' => 'accounts']));
|
||||
return;
|
||||
}
|
||||
if (!is_array($data['accounts'])) {
|
||||
$validator->errors()->add('accounts', trans('validation.filled', ['attribute' => 'accounts']));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -47,7 +47,7 @@ class DateRequest extends FormRequest
|
||||
{
|
||||
return [
|
||||
'start' => $this->getCarbonDate('start'),
|
||||
'end' => $this->getCarbonDate('end'),
|
||||
'end' => $this->getCarbonDate('end')->endOfDay(),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -24,15 +24,84 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Api\V2\Request\Transaction;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Support\Http\Api\TransactionFilter;
|
||||
use FireflyIII\Support\Request\ChecksLogin;
|
||||
use FireflyIII\Support\Request\ConvertsDataTypes;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
/**
|
||||
* Class ListRequest
|
||||
* Used specifically to list transactions.
|
||||
*/
|
||||
class ListRequest extends FormRequest
|
||||
{
|
||||
use ChecksLogin;
|
||||
use ConvertsDataTypes;
|
||||
use TransactionFilter;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function buildParams(): string
|
||||
{
|
||||
$array = [
|
||||
'page' => $this->getPage(),
|
||||
];
|
||||
|
||||
$start = $this->getStartDate();
|
||||
$end = $this->getEndDate();
|
||||
if (null !== $start && null !== $end) {
|
||||
$array['start'] = $start->format('Y-m-d');
|
||||
$array['end'] = $end->format('Y-m-d');
|
||||
}
|
||||
if (0 !== $this->getLimit()) {
|
||||
$array['limit'] = $this->getLimit();
|
||||
}
|
||||
return http_build_query($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getPage(): int
|
||||
{
|
||||
$page = $this->convertInteger('page');
|
||||
return 0 === $page || $page > 65536 ? 1 : $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Carbon|null
|
||||
*/
|
||||
public function getStartDate(): ?Carbon
|
||||
{
|
||||
return $this->getCarbonDate('start');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Carbon|null
|
||||
*/
|
||||
public function getEndDate(): ?Carbon
|
||||
{
|
||||
return $this->getCarbonDate('end');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLimit(): int
|
||||
{
|
||||
return $this->convertInteger('limit');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getTransactionTypes(): array
|
||||
{
|
||||
$type = (string)$this->get('type', 'default');
|
||||
return $this->mapTransactionTypes($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
|
@ -32,6 +32,7 @@ use FireflyIII\Models\Bill;
|
||||
use FireflyIII\Models\Budget;
|
||||
use FireflyIII\Models\Category;
|
||||
use FireflyIII\Models\CurrencyExchangeRate;
|
||||
use FireflyIII\Models\ObjectGroup;
|
||||
use FireflyIII\Models\Recurrence;
|
||||
use FireflyIII\Models\Rule;
|
||||
use FireflyIII\Models\RuleGroup;
|
||||
@ -93,6 +94,7 @@ class UpdateGroupInformation extends Command
|
||||
Bill::class,
|
||||
Budget::class,
|
||||
Category::class,
|
||||
ObjectGroup::class,
|
||||
CurrencyExchangeRate::class,
|
||||
Recurrence::class,
|
||||
RuleGroup::class,
|
||||
|
@ -69,6 +69,9 @@ class UpgradeDatabase extends Command
|
||||
'firefly-iii:liabilities-600',
|
||||
'firefly-iii:budget-limit-periods',
|
||||
'firefly-iii:restore-oauth-keys',
|
||||
// also just in case, some integrity commands:
|
||||
'firefly-iii:create-group-memberships',
|
||||
'firefly-iii:upgrade-group-information',
|
||||
];
|
||||
$args = [];
|
||||
if ($this->option('force')) {
|
||||
|
@ -92,6 +92,7 @@ class AccountFactory
|
||||
$return = $this->create(
|
||||
[
|
||||
'user_id' => $this->user->id,
|
||||
'user_group_id' => $this->user->user_group_id,
|
||||
'name' => $accountName,
|
||||
'account_type_id' => $type->id,
|
||||
'account_type_name' => null,
|
||||
@ -199,6 +200,7 @@ class AccountFactory
|
||||
$active = array_key_exists('active', $data) ? $data['active'] : true;
|
||||
$databaseData = [
|
||||
'user_id' => $this->user->id,
|
||||
'user_group_id' => $this->user->user_group_id,
|
||||
'account_type_id' => $type->id,
|
||||
'name' => $data['name'],
|
||||
'order' => 25000,
|
||||
|
@ -67,6 +67,7 @@ class BillFactory
|
||||
'match' => 'MIGRATED_TO_RULES',
|
||||
'amount_min' => $data['amount_min'],
|
||||
'user_id' => $this->user->id,
|
||||
'user_group_id' => $this->user->user_group_id,
|
||||
'transaction_currency_id' => $currency->id,
|
||||
'amount_max' => $data['amount_max'],
|
||||
'date' => $data['date'],
|
||||
|
@ -70,8 +70,9 @@ class CategoryFactory
|
||||
try {
|
||||
return Category::create(
|
||||
[
|
||||
'user_id' => $this->user->id,
|
||||
'name' => $categoryName,
|
||||
'user_id' => $this->user->id,
|
||||
'user_group_id' => $this->user->user_group_id,
|
||||
'name' => $categoryName,
|
||||
]
|
||||
);
|
||||
} catch (QueryException $e) {
|
||||
|
@ -107,6 +107,7 @@ class RecurrenceFactory
|
||||
$recurrence = new Recurrence(
|
||||
[
|
||||
'user_id' => $this->user->id,
|
||||
'user_group_id' => $this->user->user_group_id,
|
||||
'transaction_type_id' => $type->id,
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
|
@ -83,14 +83,15 @@ class TagFactory
|
||||
$latitude = 0.0 === (float)$data['latitude'] ? null : (float)$data['latitude']; // intentional float
|
||||
$longitude = 0.0 === (float)$data['longitude'] ? null : (float)$data['longitude']; // intentional float
|
||||
$array = [
|
||||
'user_id' => $this->user->id,
|
||||
'tag' => trim($data['tag']),
|
||||
'tagMode' => 'nothing',
|
||||
'date' => $data['date'],
|
||||
'description' => $data['description'],
|
||||
'latitude' => null,
|
||||
'longitude' => null,
|
||||
'zoomLevel' => null,
|
||||
'user_id' => $this->user->id,
|
||||
'user_group_id' => $this->user->user_group_id,
|
||||
'tag' => trim($data['tag']),
|
||||
'tagMode' => 'nothing',
|
||||
'date' => $data['date'],
|
||||
'description' => $data['description'],
|
||||
'latitude' => null,
|
||||
'longitude' => null,
|
||||
'zoomLevel' => null,
|
||||
];
|
||||
$tag = Tag::create($array);
|
||||
if (null !== $tag && null !== $latitude && null !== $longitude) {
|
||||
|
@ -81,6 +81,7 @@ class TransactionGroupFactory
|
||||
|
||||
$group = new TransactionGroup();
|
||||
$group->user()->associate($this->user);
|
||||
$group->userGroup()->associate($this->user->userGroup);
|
||||
$group->title = $title;
|
||||
$group->save();
|
||||
|
||||
|
@ -225,6 +225,7 @@ class TransactionJournalFactory
|
||||
$journal = TransactionJournal::create(
|
||||
[
|
||||
'user_id' => $this->user->id,
|
||||
'user_group_id' => $this->user->user_group_id,
|
||||
'transaction_type_id' => $type->id,
|
||||
'bill_id' => $billId,
|
||||
'transaction_currency_id' => $currency->id,
|
||||
@ -533,7 +534,7 @@ class TransactionJournalFactory
|
||||
{
|
||||
$description = '' === $description ? '(empty description)' : $description;
|
||||
|
||||
return substr($description, 0, 255);
|
||||
return substr($description, 0, 1024);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -131,6 +131,7 @@ class BudgetLimitHandler
|
||||
$availableBudget = new AvailableBudget(
|
||||
[
|
||||
'user_id' => $budgetLimit->budget->user->id,
|
||||
'user_group_id' => $budgetLimit->budget->user->user_group_id,
|
||||
'transaction_currency_id' => $budgetLimit->transaction_currency_id,
|
||||
'start_date' => $current,
|
||||
'end_date' => $currentEnd,
|
||||
|
@ -24,6 +24,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Helpers\Collector\Extensions;
|
||||
|
||||
use FireflyIII\Models\UserGroup;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
@ -33,22 +34,29 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
trait CollectorProperties
|
||||
{
|
||||
public const TEST = 'Test';
|
||||
private bool $expandGroupSearch;
|
||||
private array $fields;
|
||||
private bool $hasAccountInfo;
|
||||
private bool $hasBillInformation;
|
||||
private bool $hasBudgetInformation;
|
||||
private bool $hasCatInformation;
|
||||
private bool $hasJoinedAttTables;
|
||||
private bool $hasJoinedMetaTables;
|
||||
private bool $hasJoinedTagTables;
|
||||
private bool $hasNotesInformation;
|
||||
private array $integerFields;
|
||||
private ?int $limit;
|
||||
private ?int $page;
|
||||
private array $postFilters;
|
||||
private bool $expandGroupSearch;
|
||||
private array $fields;
|
||||
private bool $hasAccountInfo;
|
||||
private bool $hasBillInformation;
|
||||
private bool $hasBudgetInformation;
|
||||
private bool $hasCatInformation;
|
||||
private bool $hasJoinedAttTables;
|
||||
private bool $hasJoinedMetaTables;
|
||||
private bool $hasJoinedTagTables;
|
||||
private bool $hasNotesInformation;
|
||||
private array $integerFields;
|
||||
private ?int $limit;
|
||||
private ?int $page;
|
||||
private array $postFilters;
|
||||
private HasMany $query;
|
||||
private array $stringFields;
|
||||
private int $total;
|
||||
private ?User $user;
|
||||
private array $stringFields;
|
||||
/*
|
||||
* This array is used to collect ALL tags the user may search for (using 'setTags').
|
||||
* This way the user can call 'setTags' multiple times and get a joined result.
|
||||
*
|
||||
*/
|
||||
private array $tags;
|
||||
private int $total;
|
||||
private ?User $user;
|
||||
private ?UserGroup $userGroup;
|
||||
}
|
||||
|
@ -64,8 +64,8 @@ trait MetaCollection
|
||||
// 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->fields[] = 'bills.id as bill_id';
|
||||
$this->fields[] = 'bills.name as bill_name';
|
||||
$this->hasBillInformation = true;
|
||||
}
|
||||
|
||||
@ -104,8 +104,8 @@ trait MetaCollection
|
||||
// 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->fields[] = 'budgets.id as budget_id';
|
||||
$this->fields[] = 'budgets.name as budget_name';
|
||||
$this->hasBudgetInformation = true;
|
||||
}
|
||||
|
||||
@ -157,8 +157,8 @@ trait MetaCollection
|
||||
// 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->fields[] = 'categories.id as category_id';
|
||||
$this->fields[] = 'categories.name as category_name';
|
||||
$this->hasCatInformation = true;
|
||||
}
|
||||
|
||||
@ -603,7 +603,7 @@ trait MetaCollection
|
||||
}
|
||||
);
|
||||
// add fields
|
||||
$this->fields[] = 'notes.text as notes';
|
||||
$this->fields[] = 'notes.text as notes';
|
||||
$this->hasNotesInformation = true;
|
||||
}
|
||||
|
||||
@ -896,7 +896,8 @@ trait MetaCollection
|
||||
public function setTags(Collection $tags): GroupCollectorInterface
|
||||
{
|
||||
$this->withTagInformation();
|
||||
$this->query->whereIn('tag_transaction_journal.tag_id', $tags->pluck('id')->toArray());
|
||||
$this->tags = array_merge($this->tags, $tags->pluck('id')->toArray());
|
||||
$this->query->whereIn('tag_transaction_journal.tag_id', $this->tags);
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -913,8 +914,8 @@ trait MetaCollection
|
||||
$this->withTagInformation();
|
||||
|
||||
// this method adds a "postFilter" to the collector.
|
||||
$list = $tags->pluck('tag')->toArray();
|
||||
$filter = function (int $index, array $object) use ($list): bool {
|
||||
$list = $tags->pluck('tag')->toArray();
|
||||
$filter = function (int $index, array $object) use ($list): bool {
|
||||
foreach ($object['transactions'] as $transaction) {
|
||||
foreach ($transaction['tags'] as $tag) {
|
||||
if (in_array($tag['name'], $list, true)) {
|
||||
|
@ -675,6 +675,23 @@ trait TimeCollection
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the end time of the results to return.
|
||||
*
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return GroupCollectorInterface
|
||||
*/
|
||||
public function setEnd(Carbon $end): GroupCollectorInterface
|
||||
{
|
||||
// always got to end of day / start of day for ranges.
|
||||
$endStr = $end->format('Y-m-d 23:59:59');
|
||||
|
||||
$this->query->where('transaction_journals.date', '<=', $endStr);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Carbon $date
|
||||
* @param string $field
|
||||
@ -822,6 +839,22 @@ trait TimeCollection
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the start time of the results to return.
|
||||
*
|
||||
* @param Carbon $start
|
||||
*
|
||||
* @return GroupCollectorInterface
|
||||
*/
|
||||
public function setStart(Carbon $start): GroupCollectorInterface
|
||||
{
|
||||
$startStr = $start->format('Y-m-d 00:00:00');
|
||||
|
||||
$this->query->where('transaction_journals.date', '>=', $startStr);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect transactions updated on a specific date.
|
||||
*
|
||||
|
@ -38,6 +38,7 @@ use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Models\TransactionGroup;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use FireflyIII\Models\UserGroup;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
@ -65,7 +66,9 @@ class GroupCollector implements GroupCollectorInterface
|
||||
public function __construct()
|
||||
{
|
||||
$this->postFilters = [];
|
||||
$this->tags = [];
|
||||
$this->user = null;
|
||||
$this->userGroup = null;
|
||||
$this->limit = null;
|
||||
$this->page = null;
|
||||
|
||||
@ -81,6 +84,7 @@ class GroupCollector implements GroupCollectorInterface
|
||||
$this->integerFields = [
|
||||
'transaction_group_id',
|
||||
'user_id',
|
||||
'user_group_id',
|
||||
'transaction_journal_id',
|
||||
'transaction_type_id',
|
||||
'order',
|
||||
@ -101,6 +105,7 @@ class GroupCollector implements GroupCollectorInterface
|
||||
# group
|
||||
'transaction_groups.id as transaction_group_id',
|
||||
'transaction_groups.user_id as user_id',
|
||||
'transaction_groups.user_group_id as user_group_id',
|
||||
'transaction_groups.created_at as created_at',
|
||||
'transaction_groups.updated_at as updated_at',
|
||||
'transaction_groups.title as transaction_group_title',
|
||||
@ -299,7 +304,20 @@ class GroupCollector implements GroupCollectorInterface
|
||||
*/
|
||||
public function dumpQuery(): void
|
||||
{
|
||||
echo $this->query->select($this->fields)->toSql();
|
||||
$query = $this->query->select($this->fields)->toSql();
|
||||
$params = $this->query->getBindings();
|
||||
foreach ($params as $param) {
|
||||
$replace = sprintf('"%s"', $param);
|
||||
if (is_int($param)) {
|
||||
$replace = (string)$param;
|
||||
}
|
||||
$pos = strpos($query, '?');
|
||||
if ($pos !== false) {
|
||||
$query = substr_replace($query, $replace, $pos, 1);
|
||||
}
|
||||
}
|
||||
echo $query;
|
||||
|
||||
echo '<pre>';
|
||||
print_r($this->query->getBindings());
|
||||
echo '</pre>';
|
||||
@ -547,6 +565,7 @@ class GroupCollector implements GroupCollectorInterface
|
||||
$groupArray = [
|
||||
'id' => (int)$augumentedJournal->transaction_group_id,
|
||||
'user_id' => (int)$augumentedJournal->user_id,
|
||||
'user_group_id' => (int)$augumentedJournal->user_group_id,
|
||||
// Field transaction_group_title was added by the query.
|
||||
'title' => $augumentedJournal->transaction_group_title, // @phpstan-ignore-line
|
||||
'transaction_type' => $parsedGroup['transaction_type_type'],
|
||||
@ -1086,6 +1105,64 @@ class GroupCollector implements GroupCollectorInterface
|
||||
->orderBy('source.amount', 'DESC');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the user object and start the query.
|
||||
*
|
||||
* @param User $user
|
||||
*
|
||||
* @return GroupCollectorInterface
|
||||
*/
|
||||
public function setUserGroup(UserGroup $userGroup): GroupCollectorInterface
|
||||
{
|
||||
if (null === $this->userGroup) {
|
||||
$this->userGroup = $userGroup;
|
||||
$this->startQueryForGroup();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the query.
|
||||
*/
|
||||
private function startQueryForGroup(): void
|
||||
{
|
||||
//app('log')->debug('GroupCollector::startQuery');
|
||||
$this->query = $this->userGroup
|
||||
->transactionJournals()
|
||||
->leftJoin('transaction_groups', '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.
|
||||
*
|
||||
|
@ -30,6 +30,7 @@ use FireflyIII\Models\Category;
|
||||
use FireflyIII\Models\Tag;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Models\TransactionGroup;
|
||||
use FireflyIII\Models\UserGroup;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
@ -1077,6 +1078,15 @@ interface GroupCollectorInterface
|
||||
*/
|
||||
public function setDestinationAccounts(Collection $accounts): GroupCollectorInterface;
|
||||
|
||||
/**
|
||||
* Set the end time of the results to return.
|
||||
*
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return GroupCollectorInterface
|
||||
*/
|
||||
public function setEnd(Carbon $end): GroupCollectorInterface;
|
||||
|
||||
/**
|
||||
* @param bool $expandGroupSearch
|
||||
*/
|
||||
@ -1261,6 +1271,15 @@ interface GroupCollectorInterface
|
||||
*/
|
||||
public function setSourceAccounts(Collection $accounts): GroupCollectorInterface;
|
||||
|
||||
/**
|
||||
* Set the start time of the results to return.
|
||||
*
|
||||
* @param Carbon $start
|
||||
*
|
||||
* @return GroupCollectorInterface
|
||||
*/
|
||||
public function setStart(Carbon $start): GroupCollectorInterface;
|
||||
|
||||
/**
|
||||
* Limit results to a specific tag.
|
||||
*
|
||||
@ -1315,6 +1334,15 @@ interface GroupCollectorInterface
|
||||
*/
|
||||
public function setUser(User $user): GroupCollectorInterface;
|
||||
|
||||
/**
|
||||
* Set the user group object and start the query.
|
||||
*
|
||||
* @param UserGroup $userGroup
|
||||
*
|
||||
* @return GroupCollectorInterface
|
||||
*/
|
||||
public function setUserGroup(UserGroup $userGroup): GroupCollectorInterface;
|
||||
|
||||
/**
|
||||
* Only when does not have these tags
|
||||
*
|
||||
|
@ -27,9 +27,12 @@ use Carbon\Carbon;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Models\UserGroup;
|
||||
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
||||
use FireflyIII\Repositories\Administration\Account\AccountRepositoryInterface as AdminAccountRepositoryInterface;
|
||||
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
|
||||
use FireflyIII\Support\CacheProperties;
|
||||
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Support\Collection;
|
||||
@ -41,10 +44,98 @@ use JsonException;
|
||||
*/
|
||||
class NetWorth implements NetWorthInterface
|
||||
{
|
||||
private AccountRepositoryInterface $accountRepository;
|
||||
private AccountRepositoryInterface $accountRepository;
|
||||
private AdminAccountRepositoryInterface $adminAccountRepository;
|
||||
|
||||
private CurrencyRepositoryInterface $currencyRepos;
|
||||
private User $user;
|
||||
private UserGroup $userGroup;
|
||||
|
||||
/**
|
||||
* @param Collection $accounts
|
||||
* @param Carbon $date
|
||||
*
|
||||
* @return array
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function byAccounts(Collection $accounts, Carbon $date): array
|
||||
{
|
||||
// start in the past, end in the future? use $date
|
||||
$ids = implode(',', $accounts->pluck('id')->toArray());
|
||||
$cache = new CacheProperties();
|
||||
$cache->addProperty($date);
|
||||
$cache->addProperty('net-worth-by-accounts');
|
||||
$cache->addProperty($ids);
|
||||
if ($cache->has()) {
|
||||
//return $cache->get();
|
||||
}
|
||||
app('log')->debug(sprintf('Now in byAccounts("%s", "%s")', $ids, $date->format('Y-m-d')));
|
||||
|
||||
$default = app('amount')->getDefaultCurrency();
|
||||
$converter = new ExchangeRateConverter();
|
||||
|
||||
// default "native" currency has everything twice, for consistency.
|
||||
$netWorth = [
|
||||
'native' => [
|
||||
'balance' => '0',
|
||||
'native_balance' => '0',
|
||||
'currency_id' => (int)$default->id,
|
||||
'currency_code' => $default->code,
|
||||
'currency_name' => $default->name,
|
||||
'currency_symbol' => $default->symbol,
|
||||
'currency_decimal_places' => (int)$default->decimal_places,
|
||||
'native_id' => (int)$default->id,
|
||||
'native_code' => $default->code,
|
||||
'native_name' => $default->name,
|
||||
'native_symbol' => $default->symbol,
|
||||
'native_decimal_places' => (int)$default->decimal_places,
|
||||
],
|
||||
];
|
||||
$balances = app('steam')->balancesByAccountsConverted($accounts, $date);
|
||||
|
||||
/** @var Account $account */
|
||||
foreach ($accounts as $account) {
|
||||
app('log')->debug(sprintf('Now at account #%d ("%s")', $account->id, $account->name));
|
||||
$currency = $this->adminAccountRepository->getAccountCurrency($account);
|
||||
$currencyId = (int)$currency->id;
|
||||
$balance = '0';
|
||||
$nativeBalance = '0';
|
||||
if (array_key_exists((int)$account->id, $balances)) {
|
||||
$balance = $balances[(int)$account->id]['balance'] ?? '0';
|
||||
$nativeBalance = $balances[(int)$account->id]['native_balance'] ?? '0';
|
||||
}
|
||||
app('log')->debug(sprintf('Balance is %s, native balance is %s', $balance, $nativeBalance));
|
||||
// always subtract virtual balance
|
||||
$virtualBalance = (string)$account->virtual_balance;
|
||||
if ('' !== $virtualBalance) {
|
||||
$balance = bcsub($balance, $virtualBalance);
|
||||
$nativeVirtualBalance = $converter->convert($default, $currency, $account->created_at, $virtualBalance);
|
||||
$nativeBalance = bcsub($nativeBalance, $nativeVirtualBalance);
|
||||
}
|
||||
$netWorth[$currencyId] = $netWorth[$currencyId] ?? [
|
||||
'balance' => '0',
|
||||
'native_balance' => '0',
|
||||
'currency_id' => $currencyId,
|
||||
'currency_code' => $currency->code,
|
||||
'currency_name' => $currency->name,
|
||||
'currency_symbol' => $currency->symbol,
|
||||
'currency_decimal_places' => (int)$currency->decimal_places,
|
||||
'native_id' => (int)$default->id,
|
||||
'native_code' => $default->code,
|
||||
'native_name' => $default->name,
|
||||
'native_symbol' => $default->symbol,
|
||||
'native_decimal_places' => (int)$default->decimal_places,
|
||||
];
|
||||
|
||||
$netWorth[$currencyId]['balance'] = bcadd($balance, $netWorth[$currencyId]['balance']);
|
||||
$netWorth[$currencyId]['native_balance'] = bcadd($nativeBalance, $netWorth[$currencyId]['native_balance']);
|
||||
$netWorth['native']['balance'] = bcadd($nativeBalance, $netWorth['native']['balance']);
|
||||
$netWorth['native']['native_balance'] = bcadd($nativeBalance, $netWorth['native']['native_balance']);
|
||||
}
|
||||
$cache->store($netWorth);
|
||||
|
||||
return $netWorth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user's net worth in an array with the following layout:
|
||||
@ -146,6 +237,17 @@ class NetWorth implements NetWorthInterface
|
||||
$this->currencyRepos->setUser($this->user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function setUserGroup(UserGroup $userGroup): void
|
||||
{
|
||||
$this->userGroup = $userGroup;
|
||||
$this->adminAccountRepository = app(AdminAccountRepositoryInterface::class);
|
||||
$this->adminAccountRepository->setAdministrationId($userGroup->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
@ -24,6 +24,7 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Helpers\Report;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Models\UserGroup;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Support\Collection;
|
||||
@ -34,6 +35,21 @@ use Illuminate\Support\Collection;
|
||||
*/
|
||||
interface NetWorthInterface
|
||||
{
|
||||
/**
|
||||
* Collect net worth based on the given set of accounts.
|
||||
*
|
||||
* Returns X arrays with the net worth in each given currency, and the net worth in
|
||||
* of that amount in the native currency.
|
||||
*
|
||||
* Includes extra array with the total(!) net worth in the native currency.
|
||||
*
|
||||
* @param Collection $accounts
|
||||
* @param Carbon $date
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function byAccounts(Collection $accounts, Carbon $date): array;
|
||||
|
||||
/**
|
||||
* TODO unsure why this is deprecated.
|
||||
*
|
||||
@ -60,6 +76,11 @@ interface NetWorthInterface
|
||||
*/
|
||||
public function setUser(User | Authenticatable | null $user): void;
|
||||
|
||||
/**
|
||||
* @param UserGroup $userGroup
|
||||
*/
|
||||
public function setUserGroup(UserGroup $userGroup): void;
|
||||
|
||||
/**
|
||||
* TODO move to repository
|
||||
*
|
||||
|
@ -198,6 +198,11 @@ class ShowController extends Controller
|
||||
/** @var GroupCollectorInterface $collector */
|
||||
$collector = app(GroupCollectorInterface::class);
|
||||
$collector->setAccounts(new Collection([$account]))->setLimit($pageSize)->setPage($page)->withAccountInformation()->withCategoryInformation();
|
||||
|
||||
// this search will not include transaction groups where this asset account (or liability)
|
||||
// is just part of ONE of the journals. To force this:
|
||||
$collector->setExpandGroupSearch(true);
|
||||
|
||||
$groups = $collector->getPaginatedGroups();
|
||||
$groups->setPath(route('accounts.show.all', [$account->id]));
|
||||
$chartUrl = route('chart.account.period', [$account->id, $start->format('Y-m-d'), $end->format('Y-m-d')]);
|
||||
|
@ -90,7 +90,7 @@ class LoginController extends Controller
|
||||
Log::info('User is trying to login.');
|
||||
|
||||
$this->validateLogin($request);
|
||||
Log::debug('Login data is valid.');
|
||||
Log::debug('Login data is present.');
|
||||
|
||||
/** Copied directly from AuthenticatesUsers, but with logging added: */
|
||||
// If the class is using the ThrottlesLogins trait, we can automatically throttle
|
||||
|
@ -26,6 +26,7 @@ namespace FireflyIII\Http\Middleware;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Vite;
|
||||
|
||||
/**
|
||||
*
|
||||
@ -46,6 +47,7 @@ class SecureHeaders
|
||||
{
|
||||
// generate and share nonce.
|
||||
$nonce = base64_encode(random_bytes(16));
|
||||
Vite::useCspNonce($nonce);
|
||||
app('view')->share('JS_NONCE', $nonce);
|
||||
|
||||
$response = $next($request);
|
||||
|
@ -125,7 +125,7 @@ class Account extends Model
|
||||
'encrypted' => 'boolean',
|
||||
];
|
||||
/** @var array Fields that can be filled */
|
||||
protected $fillable = ['user_id', 'account_type_id', 'name', 'active', 'virtual_balance', 'iban'];
|
||||
protected $fillable = ['user_id', 'user_group_id', 'account_type_id', 'name', 'active', 'virtual_balance', 'iban'];
|
||||
/** @var array Hidden from view */
|
||||
protected $hidden = ['encrypted'];
|
||||
private bool $joinedAccountTypes = false;
|
||||
|
@ -85,7 +85,7 @@ class AvailableBudget extends Model
|
||||
'transaction_currency_id' => 'int',
|
||||
];
|
||||
/** @var array Fields that can be filled */
|
||||
protected $fillable = ['user_id', 'transaction_currency_id', 'amount', 'start_date', 'end_date'];
|
||||
protected $fillable = ['user_id', 'user_group_id', 'transaction_currency_id', 'amount', 'start_date', 'end_date'];
|
||||
|
||||
/**
|
||||
* Route binder. Converts the key in the URL to the specified object (or throw 404).
|
||||
|
@ -130,6 +130,7 @@ class Bill extends Model
|
||||
'match',
|
||||
'amount_min',
|
||||
'user_id',
|
||||
'user_group_id',
|
||||
'amount_max',
|
||||
'date',
|
||||
'repeat_freq',
|
||||
|
@ -99,7 +99,7 @@ class Budget extends Model
|
||||
'encrypted' => 'boolean',
|
||||
];
|
||||
/** @var array Fields that can be filled */
|
||||
protected $fillable = ['user_id', 'name', 'active', 'order'];
|
||||
protected $fillable = ['user_id', 'name', 'active', 'order', 'user_group_id'];
|
||||
/** @var array Hidden from view */
|
||||
protected $hidden = ['encrypted'];
|
||||
|
||||
|
@ -89,7 +89,7 @@ class Category extends Model
|
||||
'encrypted' => 'boolean',
|
||||
];
|
||||
/** @var array Fields that can be filled */
|
||||
protected $fillable = ['user_id', 'name'];
|
||||
protected $fillable = ['user_id', 'user_group_id', 'name'];
|
||||
/** @var array Hidden from view */
|
||||
protected $hidden = ['encrypted'];
|
||||
|
||||
|
@ -77,7 +77,7 @@ class ObjectGroup extends Model
|
||||
'user_id' => 'integer',
|
||||
'deleted_at' => 'datetime',
|
||||
];
|
||||
protected $fillable = ['title', 'order', 'user_id'];
|
||||
protected $fillable = ['title', 'order', 'user_id', 'user_group_id'];
|
||||
|
||||
/**
|
||||
* Route binder. Converts the key in the URL to the specified object (or throw 404).
|
||||
|
@ -165,4 +165,12 @@ class Rule extends Model
|
||||
{
|
||||
$this->attributes['description'] = e($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo
|
||||
*/
|
||||
public function userGroup(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(UserGroup::class);
|
||||
}
|
||||
}
|
||||
|
@ -99,7 +99,7 @@ class Tag extends Model
|
||||
'longitude' => 'float',
|
||||
];
|
||||
/** @var array Fields that can be filled */
|
||||
protected $fillable = ['user_id', 'tag', 'date', 'description', 'tagMode'];
|
||||
protected $fillable = ['user_id', 'user_group_id', 'tag', 'date', 'description', 'tagMode'];
|
||||
|
||||
protected $hidden = ['zoomLevel', 'latitude', 'longitude'];
|
||||
|
||||
|
@ -130,4 +130,12 @@ class TransactionGroup extends Model
|
||||
{
|
||||
return $this->hasMany(TransactionJournal::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo
|
||||
*/
|
||||
public function userGroup(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(UserGroup::class);
|
||||
}
|
||||
}
|
||||
|
@ -148,6 +148,7 @@ class TransactionJournal extends Model
|
||||
protected $fillable
|
||||
= [
|
||||
'user_id',
|
||||
'user_group_id',
|
||||
'transaction_type_id',
|
||||
'bill_id',
|
||||
'tag_count',
|
||||
|
@ -29,6 +29,7 @@ use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
@ -67,6 +68,36 @@ class UserGroup extends Model
|
||||
return $this->hasMany(Account::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Link to bills.
|
||||
*
|
||||
* @return HasMany
|
||||
*/
|
||||
public function availableBudgets(): HasMany
|
||||
{
|
||||
return $this->hasMany(AvailableBudget::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Link to bills.
|
||||
*
|
||||
* @return HasMany
|
||||
*/
|
||||
public function bills(): HasMany
|
||||
{
|
||||
return $this->hasMany(Bill::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Link to budgets.
|
||||
*
|
||||
* @return HasMany
|
||||
*/
|
||||
public function budgets(): HasMany
|
||||
{
|
||||
return $this->hasMany(Budget::class);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return HasMany
|
||||
@ -75,4 +106,24 @@ class UserGroup extends Model
|
||||
{
|
||||
return $this->hasMany(GroupMembership::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Link to piggy banks.
|
||||
*
|
||||
* @return HasManyThrough
|
||||
*/
|
||||
public function piggyBanks(): HasManyThrough
|
||||
{
|
||||
return $this->hasManyThrough(PiggyBank::class, Account::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Link to transaction journals.
|
||||
*
|
||||
* @return HasMany
|
||||
*/
|
||||
public function transactionJournals(): HasMany
|
||||
{
|
||||
return $this->hasMany(TransactionJournal::class);
|
||||
}
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ class Webhook extends Model
|
||||
'response' => 'integer',
|
||||
'delivery' => 'integer',
|
||||
];
|
||||
protected $fillable = ['active', 'trigger', 'response', 'delivery', 'user_id', 'url', 'title', 'secret'];
|
||||
protected $fillable = ['active', 'trigger', 'response', 'delivery', 'user_id', 'user_group_id', 'url', 'title', 'secret'];
|
||||
|
||||
/**
|
||||
* @return array
|
||||
|
@ -25,6 +25,8 @@ namespace FireflyIII\Providers;
|
||||
|
||||
use FireflyIII\Repositories\Bill\BillRepository;
|
||||
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
|
||||
use FireflyIII\Repositories\Administration\Bill\BillRepository as AdminBillRepository;
|
||||
use FireflyIII\Repositories\Administration\Bill\BillRepositoryInterface as AdminBillRepositoryInterface;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
@ -59,5 +61,21 @@ class BillServiceProvider extends ServiceProvider
|
||||
return $repository;
|
||||
}
|
||||
);
|
||||
|
||||
// administration variant
|
||||
$this->app->bind(
|
||||
AdminBillRepositoryInterface::class,
|
||||
function (Application $app) {
|
||||
/** @var AdminBillRepositoryInterface $repository */
|
||||
$repository = app(AdminBillRepository::class);
|
||||
|
||||
// reference to auth is not understood by phpstan.
|
||||
if ($app->auth->check()) { // @phpstan-ignore-line
|
||||
$repository->setUser(auth()->user());
|
||||
}
|
||||
|
||||
return $repository;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -25,14 +25,20 @@ namespace FireflyIII\Providers;
|
||||
|
||||
use FireflyIII\Repositories\Budget\AvailableBudgetRepository;
|
||||
use FireflyIII\Repositories\Budget\AvailableBudgetRepositoryInterface;
|
||||
use FireflyIII\Repositories\Administration\Budget\AvailableBudgetRepository as AdminAbRepository;
|
||||
use FireflyIII\Repositories\Administration\Budget\AvailableBudgetRepositoryInterface as AdminAbRepositoryInterface;
|
||||
use FireflyIII\Repositories\Budget\BudgetLimitRepository;
|
||||
use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface;
|
||||
use FireflyIII\Repositories\Budget\BudgetRepository;
|
||||
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
|
||||
use FireflyIII\Repositories\Administration\Budget\BudgetRepository as AdminBudgetRepository;
|
||||
use FireflyIII\Repositories\Administration\Budget\BudgetRepositoryInterface as AdminBudgetRepositoryInterface;
|
||||
use FireflyIII\Repositories\Budget\NoBudgetRepository;
|
||||
use FireflyIII\Repositories\Budget\NoBudgetRepositoryInterface;
|
||||
use FireflyIII\Repositories\Budget\OperationsRepository;
|
||||
use FireflyIII\Repositories\Budget\OperationsRepositoryInterface;
|
||||
use FireflyIII\Repositories\Administration\Budget\OperationsRepository as AdminOperationsRepository;
|
||||
use FireflyIII\Repositories\Administration\Budget\OperationsRepositoryInterface as AdminOperationsRepositoryInterface;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
@ -54,7 +60,6 @@ class BudgetServiceProvider extends ServiceProvider
|
||||
public function register(): void
|
||||
{
|
||||
// reference to auth is not understood by phpstan.
|
||||
|
||||
$this->app->bind(
|
||||
BudgetRepositoryInterface::class,
|
||||
static function (Application $app) {
|
||||
@ -68,6 +73,20 @@ class BudgetServiceProvider extends ServiceProvider
|
||||
}
|
||||
);
|
||||
|
||||
$this->app->bind(
|
||||
AdminBudgetRepositoryInterface::class,
|
||||
static function (Application $app) {
|
||||
/** @var AdminBudgetRepositoryInterface $repository */
|
||||
$repository = app(AdminBudgetRepository::class);
|
||||
if ($app->auth->check()) { // @phpstan-ignore-line
|
||||
$repository->setUser(auth()->user());
|
||||
$repository->setAdministrationId(auth()->user()->user_group_id);
|
||||
}
|
||||
|
||||
return $repository;
|
||||
}
|
||||
);
|
||||
|
||||
// available budget repos
|
||||
$this->app->bind(
|
||||
AvailableBudgetRepositoryInterface::class,
|
||||
@ -82,6 +101,21 @@ class BudgetServiceProvider extends ServiceProvider
|
||||
}
|
||||
);
|
||||
|
||||
// available budget repos
|
||||
$this->app->bind(
|
||||
AdminAbRepositoryInterface::class,
|
||||
static function (Application $app) {
|
||||
/** @var AdminAbRepositoryInterface $repository */
|
||||
$repository = app(AdminAbRepository::class);
|
||||
if ($app->auth->check()) { // @phpstan-ignore-line
|
||||
$repository->setUser(auth()->user());
|
||||
$repository->setAdministrationId(auth()->user()->user_group_id);
|
||||
}
|
||||
|
||||
return $repository;
|
||||
}
|
||||
);
|
||||
|
||||
// budget limit repository.
|
||||
$this->app->bind(
|
||||
BudgetLimitRepositoryInterface::class,
|
||||
@ -120,6 +154,19 @@ class BudgetServiceProvider extends ServiceProvider
|
||||
$repository->setUser(auth()->user());
|
||||
}
|
||||
|
||||
return $repository;
|
||||
}
|
||||
);
|
||||
$this->app->bind(
|
||||
AdminOperationsRepositoryInterface::class,
|
||||
static function (Application $app) {
|
||||
/** @var AdminOperationsRepositoryInterface $repository */
|
||||
$repository = app(AdminOperationsRepository::class);
|
||||
if ($app->auth->check()) { // @phpstan-ignore-line
|
||||
$repository->setUser(auth()->user());
|
||||
$repository->setAdministrationId(auth()->user()->user_group_id);
|
||||
}
|
||||
|
||||
return $repository;
|
||||
}
|
||||
);
|
||||
|
@ -25,6 +25,10 @@ namespace FireflyIII\Providers;
|
||||
|
||||
use FireflyIII\Repositories\PiggyBank\PiggyBankRepository;
|
||||
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
|
||||
|
||||
use FireflyIII\Repositories\Administration\PiggyBank\PiggyBankRepository as AdminPiggyBankRepository;
|
||||
use FireflyIII\Repositories\Administration\PiggyBank\PiggyBankRepositoryInterface as AdminPiggyBankRepositoryInterface;
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
@ -57,5 +61,17 @@ class PiggyBankServiceProvider extends ServiceProvider
|
||||
return $repository;
|
||||
}
|
||||
);
|
||||
|
||||
$this->app->bind(
|
||||
AdminPiggyBankRepositoryInterface::class,
|
||||
function (Application $app) {
|
||||
/** @var AdminPiggyBankRepository $repository */
|
||||
$repository = app(AdminPiggyBankRepository::class);
|
||||
if ($app->auth->check()) { // @phpstan-ignore-line (phpstan does not understand the reference to auth)
|
||||
$repository->setUser(auth()->user());
|
||||
}
|
||||
return $repository;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ use FireflyIII\Models\AccountMeta;
|
||||
use FireflyIII\Models\AccountType;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Support\Repositories\Administration\AdministrationTrait;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
@ -39,82 +40,6 @@ class AccountRepository implements AccountRepositoryInterface
|
||||
{
|
||||
use AdministrationTrait;
|
||||
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function searchAccount(string $query, array $types, int $limit): Collection
|
||||
{
|
||||
// search by group, not by user
|
||||
$dbQuery = $this->userGroup->accounts()
|
||||
->where('active', true)
|
||||
->orderBy('accounts.order', 'ASC')
|
||||
->orderBy('accounts.account_type_id', 'ASC')
|
||||
->orderBy('accounts.name', 'ASC')
|
||||
->with(['accountType']);
|
||||
if ('' !== $query) {
|
||||
// split query on spaces just in case:
|
||||
$parts = explode(' ', $query);
|
||||
foreach ($parts as $part) {
|
||||
$search = sprintf('%%%s%%', $part);
|
||||
$dbQuery->where('name', 'LIKE', $search);
|
||||
}
|
||||
}
|
||||
if (0 !== count($types)) {
|
||||
$dbQuery->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
|
||||
$dbQuery->whereIn('account_types.type', $types);
|
||||
}
|
||||
|
||||
return $dbQuery->take($limit)->get(['accounts.*']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getAccountsByType(array $types, ?array $sort = []): Collection
|
||||
{
|
||||
$res = array_intersect([AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT], $types);
|
||||
$query = $this->userGroup->accounts();
|
||||
if (0 !== count($types)) {
|
||||
$query->accountTypeIn($types);
|
||||
}
|
||||
|
||||
// add sort parameters. At this point they're filtered to allowed fields to sort by:
|
||||
if (0 !== count($sort)) {
|
||||
foreach ($sort as $param) {
|
||||
$query->orderBy($param[0], $param[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (0 === count($sort)) {
|
||||
if (0 !== count($res)) {
|
||||
$query->orderBy('accounts.order', 'ASC');
|
||||
}
|
||||
$query->orderBy('accounts.active', 'DESC');
|
||||
$query->orderBy('accounts.name', 'ASC');
|
||||
}
|
||||
return $query->get(['accounts.*']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $accountIds
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getAccountsById(array $accountIds): Collection
|
||||
{
|
||||
$query = $this->userGroup->accounts();
|
||||
|
||||
if (0 !== count($accountIds)) {
|
||||
$query->whereIn('accounts.id', $accountIds);
|
||||
}
|
||||
$query->orderBy('accounts.order', 'ASC');
|
||||
$query->orderBy('accounts.active', 'DESC');
|
||||
$query->orderBy('accounts.name', 'ASC');
|
||||
|
||||
return $query->get(['accounts.*']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Account $account
|
||||
*
|
||||
@ -141,7 +66,7 @@ class AccountRepository implements AccountRepositoryInterface
|
||||
* Return meta value for account. Null if not found.
|
||||
*
|
||||
* @param Account $account
|
||||
* @param string $field
|
||||
* @param string $field
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
@ -161,4 +86,112 @@ class AccountRepository implements AccountRepositoryInterface
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $accountId
|
||||
*
|
||||
* @return Account|null
|
||||
*/
|
||||
public function find(int $accountId): ?Account
|
||||
{
|
||||
$account = $this->user->accounts()->find($accountId);
|
||||
if (null === $account) {
|
||||
$account = $this->userGroup->accounts()->find($accountId);
|
||||
}
|
||||
return $account;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $accountIds
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getAccountsById(array $accountIds): Collection
|
||||
{
|
||||
$query = $this->userGroup->accounts();
|
||||
|
||||
if (0 !== count($accountIds)) {
|
||||
$query->whereIn('accounts.id', $accountIds);
|
||||
}
|
||||
$query->orderBy('accounts.order', 'ASC');
|
||||
$query->orderBy('accounts.active', 'DESC');
|
||||
$query->orderBy('accounts.name', 'ASC');
|
||||
|
||||
return $query->get(['accounts.*']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getAccountsByType(array $types, ?array $sort = []): Collection
|
||||
{
|
||||
$res = array_intersect([AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT], $types);
|
||||
$query = $this->userGroup->accounts();
|
||||
if (0 !== count($types)) {
|
||||
$query->accountTypeIn($types);
|
||||
}
|
||||
|
||||
// add sort parameters. At this point they're filtered to allowed fields to sort by:
|
||||
if (0 !== count($sort)) {
|
||||
foreach ($sort as $param) {
|
||||
$query->orderBy($param[0], $param[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (0 === count($sort)) {
|
||||
if (0 !== count($res)) {
|
||||
$query->orderBy('accounts.order', 'ASC');
|
||||
}
|
||||
$query->orderBy('accounts.active', 'DESC');
|
||||
$query->orderBy('accounts.name', 'ASC');
|
||||
}
|
||||
return $query->get(['accounts.*']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $types
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getActiveAccountsByType(array $types): Collection
|
||||
{
|
||||
$query = $this->userGroup->accounts();
|
||||
if (0 !== count($types)) {
|
||||
$query->accountTypeIn($types);
|
||||
}
|
||||
$query->where('active', true);
|
||||
$query->orderBy('accounts.account_type_id', 'ASC');
|
||||
$query->orderBy('accounts.order', 'ASC');
|
||||
$query->orderBy('accounts.name', 'ASC');
|
||||
|
||||
return $query->get(['accounts.*']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function searchAccount(string $query, array $types, int $limit): Collection
|
||||
{
|
||||
// search by group, not by user
|
||||
$dbQuery = $this->userGroup->accounts()
|
||||
->where('active', true)
|
||||
->orderBy('accounts.order', 'ASC')
|
||||
->orderBy('accounts.account_type_id', 'ASC')
|
||||
->orderBy('accounts.name', 'ASC')
|
||||
->with(['accountType']);
|
||||
if ('' !== $query) {
|
||||
// split query on spaces just in case:
|
||||
$parts = explode(' ', $query);
|
||||
foreach ($parts as $part) {
|
||||
$search = sprintf('%%%s%%', $part);
|
||||
$dbQuery->where('name', 'LIKE', $search);
|
||||
}
|
||||
}
|
||||
if (0 !== count($types)) {
|
||||
$dbQuery->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
|
||||
$dbQuery->whereIn('account_types.type', $types);
|
||||
}
|
||||
|
||||
return $dbQuery->take($limit)->get(['accounts.*']);
|
||||
}
|
||||
}
|
||||
|
@ -35,28 +35,11 @@ use Illuminate\Support\Collection;
|
||||
interface AccountRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $types
|
||||
* @param int $limit
|
||||
* @param int $accountId
|
||||
*
|
||||
* @return Collection
|
||||
* @return Account|null
|
||||
*/
|
||||
public function searchAccount(string $query, array $types, int $limit): Collection;
|
||||
|
||||
/**
|
||||
* @param array $types
|
||||
* @param array|null $sort
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getAccountsByType(array $types, ?array $sort = []): Collection;
|
||||
|
||||
/**
|
||||
* @param array $accountIds
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getAccountsById(array $accountIds): Collection;
|
||||
public function find(int $accountId): ?Account;
|
||||
|
||||
/**
|
||||
* @param Account $account
|
||||
@ -65,13 +48,45 @@ interface AccountRepositoryInterface
|
||||
*/
|
||||
public function getAccountCurrency(Account $account): ?TransactionCurrency;
|
||||
|
||||
/**
|
||||
* @param array $accountIds
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getAccountsById(array $accountIds): Collection;
|
||||
|
||||
/**
|
||||
* @param array $types
|
||||
* @param array|null $sort
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getAccountsByType(array $types, ?array $sort = []): Collection;
|
||||
|
||||
/**
|
||||
* @param array $types
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getActiveAccountsByType(array $types): Collection;
|
||||
|
||||
/**
|
||||
* Return meta value for account. Null if not found.
|
||||
*
|
||||
* @param Account $account
|
||||
* @param string $field
|
||||
* @param string $field
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getMetaValue(Account $account, string $field): ?string;
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $types
|
||||
* @param int $limit
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function searchAccount(string $query, array $types, int $limit): Collection;
|
||||
|
||||
}
|
||||
|
246
app/Repositories/Administration/Bill/BillRepository.php
Normal file
246
app/Repositories/Administration/Bill/BillRepository.php
Normal file
@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* BillRepository.php
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Repositories\Administration\Bill;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Models\Bill;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Support\CacheProperties;
|
||||
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
|
||||
use FireflyIII\Support\Repositories\Administration\AdministrationTrait;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class BillRepository
|
||||
*/
|
||||
class BillRepository implements BillRepositoryInterface
|
||||
{
|
||||
use AdministrationTrait;
|
||||
|
||||
/**
|
||||
* Correct order of piggies in case of issues.
|
||||
*/
|
||||
public function correctOrder(): void
|
||||
{
|
||||
$set = $this->userGroup->bills()->orderBy('order', 'ASC')->get();
|
||||
$current = 1;
|
||||
foreach ($set as $bill) {
|
||||
if ((int)$bill->order !== $current) {
|
||||
$bill->order = $current;
|
||||
$bill->save();
|
||||
}
|
||||
$current++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection
|
||||
*/
|
||||
public function getBills(): Collection
|
||||
{
|
||||
return $this->userGroup->bills()
|
||||
->orderBy('bills.name', 'ASC')
|
||||
->get(['bills.*']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function sumPaidInRange(Carbon $start, Carbon $end): array
|
||||
{
|
||||
$bills = $this->getActiveBills();
|
||||
$default = app('amount')->getDefaultCurrency();
|
||||
$return = [];
|
||||
$converter = new ExchangeRateConverter();
|
||||
/** @var Bill $bill */
|
||||
foreach ($bills as $bill) {
|
||||
/** @var Collection $set */
|
||||
$set = $bill->transactionJournals()->after($start)->before($end)->get(['transaction_journals.*']);
|
||||
$currency = $bill->transactionCurrency;
|
||||
$currencyId = (int)$bill->transaction_currency_id;
|
||||
|
||||
$return[$currencyId] = $return[$currencyId] ?? [
|
||||
'currency_id' => (string)$currency->id,
|
||||
'currency_name' => $currency->name,
|
||||
'currency_symbol' => $currency->symbol,
|
||||
'currency_code' => $currency->code,
|
||||
'currency_decimal_places' => (int)$currency->decimal_places,
|
||||
'native_id' => (string)$default->id,
|
||||
'native_name' => $default->name,
|
||||
'native_symbol' => $default->symbol,
|
||||
'native_code' => $default->code,
|
||||
'native_decimal_places' => (int)$default->decimal_places,
|
||||
'sum' => '0',
|
||||
'native_sum' => '0',
|
||||
];
|
||||
|
||||
/** @var TransactionJournal $transactionJournal */
|
||||
foreach ($set as $transactionJournal) {
|
||||
/** @var Transaction|null $sourceTransaction */
|
||||
$sourceTransaction = $transactionJournal->transactions()->where('amount', '<', 0)->first();
|
||||
if (null !== $sourceTransaction) {
|
||||
$amount = (string)$sourceTransaction->amount;
|
||||
if ((int)$sourceTransaction->foreign_currency_id === (int)$currency->id) {
|
||||
// use foreign amount instead!
|
||||
$amount = (string)$sourceTransaction->foreign_amount;
|
||||
}
|
||||
// convert to native currency
|
||||
$nativeAmount = $amount;
|
||||
if ($currencyId !== (int)$default->id) {
|
||||
// get rate and convert.
|
||||
$nativeAmount = $converter->convert($currency, $default, $transactionJournal->date, $amount);
|
||||
}
|
||||
if ((int)$sourceTransaction->foreign_currency_id === (int)$default->id) {
|
||||
// ignore conversion, use foreign amount
|
||||
$nativeAmount = (string)$sourceTransaction->foreign_amount;
|
||||
}
|
||||
$return[$currencyId]['sum'] = bcadd($return[$currencyId]['sum'], $amount);
|
||||
$return[$currencyId]['native_sum'] = bcadd($return[$currencyId]['native_sum'], $nativeAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection
|
||||
*/
|
||||
public function getActiveBills(): Collection
|
||||
{
|
||||
return $this->userGroup->bills()
|
||||
->where('active', true)
|
||||
->orderBy('bills.name', 'ASC')
|
||||
->get(['bills.*']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function sumUnpaidInRange(Carbon $start, Carbon $end): array
|
||||
{
|
||||
$bills = $this->getActiveBills();
|
||||
$return = [];
|
||||
$default = app('amount')->getDefaultCurrency();
|
||||
$converter = new ExchangeRateConverter();
|
||||
/** @var Bill $bill */
|
||||
foreach ($bills as $bill) {
|
||||
$dates = $this->getPayDatesInRange($bill, $start, $end);
|
||||
$count = $bill->transactionJournals()->after($start)->before($end)->count();
|
||||
$total = $dates->count() - $count;
|
||||
|
||||
if ($total > 0) {
|
||||
$currency = $bill->transactionCurrency;
|
||||
$currencyId = (int)$bill->transaction_currency_id;
|
||||
$average = bcdiv(bcadd($bill->amount_max, $bill->amount_min), '2');
|
||||
$nativeAverage = $converter->convert($currency, $default, $start, $average);
|
||||
$return[$currencyId] = $return[$currencyId] ?? [
|
||||
'currency_id' => (string)$currency->id,
|
||||
'currency_name' => $currency->name,
|
||||
'currency_symbol' => $currency->symbol,
|
||||
'currency_code' => $currency->code,
|
||||
'currency_decimal_places' => (int)$currency->decimal_places,
|
||||
'native_id' => (string)$default->id,
|
||||
'native_name' => $default->name,
|
||||
'native_symbol' => $default->symbol,
|
||||
'native_code' => $default->code,
|
||||
'native_decimal_places' => (int)$default->decimal_places,
|
||||
'sum' => '0',
|
||||
'native_sum' => '0',
|
||||
];
|
||||
$return[$currencyId]['sum'] = bcadd($return[$currencyId]['sum'], bcmul($average, (string)$total));
|
||||
$return[$currencyId]['native_sum'] = bcadd($return[$currencyId]['native_sum'], bcmul($nativeAverage, (string)$total));
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Between start and end, tells you on which date(s) the bill is expected to hit.
|
||||
* TODO duplicate of function in other billrepositoryinterface
|
||||
*
|
||||
* @param Bill $bill
|
||||
* @param Carbon $start
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getPayDatesInRange(Bill $bill, Carbon $start, Carbon $end): Collection
|
||||
{
|
||||
$set = new Collection();
|
||||
$currentStart = clone $start;
|
||||
//Log::debug(sprintf('Now at bill "%s" (%s)', $bill->name, $bill->repeat_freq));
|
||||
//Log::debug(sprintf('First currentstart is %s', $currentStart->format('Y-m-d')));
|
||||
|
||||
while ($currentStart <= $end) {
|
||||
//Log::debug(sprintf('Currentstart is now %s.', $currentStart->format('Y-m-d')));
|
||||
$nextExpectedMatch = $this->nextDateMatch($bill, $currentStart);
|
||||
//Log::debug(sprintf('Next Date match after %s is %s', $currentStart->format('Y-m-d'), $nextExpectedMatch->format('Y-m-d')));
|
||||
if ($nextExpectedMatch > $end) {// If nextExpectedMatch is after end, we continue
|
||||
break;
|
||||
}
|
||||
$set->push(clone $nextExpectedMatch);
|
||||
//Log::debug(sprintf('Now %d dates in set.', $set->count()));
|
||||
$nextExpectedMatch->addDay();
|
||||
|
||||
//Log::debug(sprintf('Currentstart (%s) has become %s.', $currentStart->format('Y-m-d'), $nextExpectedMatch->format('Y-m-d')));
|
||||
|
||||
$currentStart = clone $nextExpectedMatch;
|
||||
}
|
||||
|
||||
return $set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a bill and a date, this method will tell you at which moment this bill expects its next
|
||||
* transaction. Whether it is there already, is not relevant.
|
||||
*
|
||||
* TODO duplicate of other repos
|
||||
*
|
||||
* @param Bill $bill
|
||||
* @param Carbon $date
|
||||
*
|
||||
* @return Carbon
|
||||
*/
|
||||
public function nextDateMatch(Bill $bill, Carbon $date): Carbon
|
||||
{
|
||||
$cache = new CacheProperties();
|
||||
$cache->addProperty($bill->id);
|
||||
$cache->addProperty('nextDateMatch');
|
||||
$cache->addProperty($date);
|
||||
if ($cache->has()) {
|
||||
return $cache->get();
|
||||
}
|
||||
// find the most recent date for this bill NOT in the future. Cache this date:
|
||||
$start = clone $bill->date;
|
||||
|
||||
while ($start < $date) {
|
||||
$start = app('navigation')->addPeriod($start, $bill->repeat_freq, $bill->skip);
|
||||
}
|
||||
$cache->store($start);
|
||||
|
||||
return $start;
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* BillRepositoryInterface.php
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Repositories\Administration\Bill;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Models\Bill;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Interface BillRepositoryInterface
|
||||
*/
|
||||
interface BillRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* TODO duplicate of other repos
|
||||
* Add correct order to bills.
|
||||
*/
|
||||
public function correctOrder(): void;
|
||||
|
||||
/**
|
||||
* @return Collection
|
||||
*/
|
||||
public function getActiveBills(): Collection;
|
||||
|
||||
/**
|
||||
* @return Collection
|
||||
*/
|
||||
public function getBills(): Collection;
|
||||
|
||||
/**
|
||||
* Between start and end, tells you on which date(s) the bill is expected to hit.
|
||||
*
|
||||
* TODO duplicate of method in other billrepositoryinterface
|
||||
*
|
||||
* @param Bill $bill
|
||||
* @param Carbon $start
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getPayDatesInRange(Bill $bill, Carbon $start, Carbon $end): Collection;
|
||||
|
||||
/**
|
||||
* Given a bill and a date, this method will tell you at which moment this bill expects its next
|
||||
* transaction. Whether it is there already, is not relevant.
|
||||
*
|
||||
* TODO duplicate of method in other bill repos
|
||||
*
|
||||
* @param Bill $bill
|
||||
* @param Carbon $date
|
||||
*
|
||||
* @return Carbon
|
||||
*/
|
||||
public function nextDateMatch(Bill $bill, Carbon $date): Carbon;
|
||||
|
||||
/**
|
||||
* Collect multi-currency of sum of bills already paid.
|
||||
*
|
||||
* @param Carbon $start
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function sumPaidInRange(Carbon $start, Carbon $end): array;
|
||||
|
||||
/**
|
||||
* Collect multi-currency of sum of bills yet to pay.
|
||||
*
|
||||
* @param Carbon $start
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function sumUnpaidInRange(Carbon $start, Carbon $end): array;
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* AvailableBudgetRepository.php
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Repositories\Administration\Budget;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Models\AvailableBudget;
|
||||
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
|
||||
use FireflyIII\Support\Repositories\Administration\AdministrationTrait;
|
||||
|
||||
/**
|
||||
* Class AvailableBudgetRepository
|
||||
*/
|
||||
class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface
|
||||
{
|
||||
use AdministrationTrait;
|
||||
|
||||
/**
|
||||
* @param Carbon $start
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAvailableBudgetWithCurrency(Carbon $start, Carbon $end): array
|
||||
{
|
||||
$return = [];
|
||||
$converter = new ExchangeRateConverter();
|
||||
$default = app('amount')->getDefaultCurrency();
|
||||
$availableBudgets = $this->userGroup->availableBudgets()
|
||||
->where('start_date', $start->format('Y-m-d'))
|
||||
->where('end_date', $end->format('Y-m-d'))->get();
|
||||
/** @var AvailableBudget $availableBudget */
|
||||
foreach ($availableBudgets as $availableBudget) {
|
||||
$currencyId = (int)$availableBudget->transaction_currency_id;
|
||||
$return[$currencyId] = $return[$currencyId] ?? [
|
||||
'currency_id' => $currencyId,
|
||||
'currency_code' => $availableBudget->transactionCurrency->code,
|
||||
'currency_symbol' => $availableBudget->transactionCurrency->symbol,
|
||||
'currency_name' => $availableBudget->transactionCurrency->name,
|
||||
'currency_decimal_places' => (int)$availableBudget->transactionCurrency->decimal_places,
|
||||
'native_id' => $default->id,
|
||||
'native_code' => $default->code,
|
||||
'native_symbol' => $default->symbol,
|
||||
'native_name' => $default->name,
|
||||
'native_decimal_places' => (int)$default->decimal_places,
|
||||
'amount' => '0',
|
||||
'native_amount' => '0',
|
||||
];
|
||||
$nativeAmount = $converter->convert($availableBudget->transactionCurrency, $default, $availableBudget->start_date, $availableBudget->amount);
|
||||
$return[$currencyId]['amount'] = bcadd($return[$currencyId]['amount'], $availableBudget->amount);
|
||||
$return[$currencyId]['native_amount'] = bcadd($return[$currencyId]['native_amount'], $nativeAmount);
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* AvailableBudgetRepositoryInterface.php
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Repositories\Administration\Budget;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* Interface AvailableBudgetRepositoryInterface
|
||||
*/
|
||||
interface AvailableBudgetRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* @param Carbon $start
|
||||
* @param Carbon $end
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAvailableBudgetWithCurrency(Carbon $start, Carbon $end): array;
|
||||
|
||||
}
|
46
app/Repositories/Administration/Budget/BudgetRepository.php
Normal file
46
app/Repositories/Administration/Budget/BudgetRepository.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* BudgetRepository.php
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Repositories\Administration\Budget;
|
||||
|
||||
use FireflyIII\Support\Repositories\Administration\AdministrationTrait;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class BudgetRepository
|
||||
*/
|
||||
class BudgetRepository implements BudgetRepositoryInterface
|
||||
{
|
||||
use AdministrationTrait;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getActiveBudgets(): Collection
|
||||
{
|
||||
return $this->userGroup->budgets()->where('active', true)
|
||||
->orderBy('order', 'ASC')
|
||||
->orderBy('name', 'ASC')
|
||||
->get();
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* BudgetRepositoryInterface.php
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Repositories\Administration\Budget;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Interface BudgetRepositoryInterface
|
||||
*/
|
||||
interface BudgetRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* @return Collection
|
||||
*/
|
||||
public function getActiveBudgets(): Collection;
|
||||
}
|
137
app/Repositories/Administration/Budget/OperationsRepository.php
Normal file
137
app/Repositories/Administration/Budget/OperationsRepository.php
Normal file
@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* OperationsRepository.php
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Repositories\Administration\Budget;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use FireflyIII\Support\Repositories\Administration\AdministrationTrait;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class OperationsRepository
|
||||
*/
|
||||
class OperationsRepository implements OperationsRepositoryInterface
|
||||
{
|
||||
use AdministrationTrait;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function listExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $budgets = null): array
|
||||
{
|
||||
/** @var GroupCollectorInterface $collector */
|
||||
$collector = app(GroupCollectorInterface::class);
|
||||
$collector->setUserGroup($this->userGroup)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL]);
|
||||
if (null !== $accounts && $accounts->count() > 0) {
|
||||
$collector->setAccounts($accounts);
|
||||
}
|
||||
if (null !== $budgets && $budgets->count() > 0) {
|
||||
$collector->setBudgets($budgets);
|
||||
}
|
||||
if (null === $budgets || (0 === $budgets->count())) {
|
||||
$collector->setBudgets($this->getBudgets());
|
||||
}
|
||||
$collector->withBudgetInformation()->withAccountInformation()->withCategoryInformation();
|
||||
$journals = $collector->getExtractedJournals();
|
||||
$array = [];
|
||||
|
||||
foreach ($journals as $journal) {
|
||||
$currencyId = (int)$journal['currency_id'];
|
||||
$budgetId = (int)$journal['budget_id'];
|
||||
$budgetName = (string)$journal['budget_name'];
|
||||
|
||||
// catch "no budget" entries.
|
||||
if (0 === $budgetId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// info about the currency:
|
||||
$array[$currencyId] = $array[$currencyId] ?? [
|
||||
'budgets' => [],
|
||||
'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'],
|
||||
];
|
||||
|
||||
// info about the budgets:
|
||||
$array[$currencyId]['budgets'][$budgetId] = $array[$currencyId]['budgets'][$budgetId] ?? [
|
||||
'id' => $budgetId,
|
||||
'name' => $budgetName,
|
||||
'transaction_journals' => [],
|
||||
];
|
||||
|
||||
// add journal to array:
|
||||
// only a subset of the fields.
|
||||
$journalId = (int)$journal['transaction_journal_id'];
|
||||
$final = [
|
||||
'amount' => app('steam')->negative($journal['amount']),
|
||||
'currency_id' => $journal['currency_id'],
|
||||
'foreign_amount' => null,
|
||||
'foreign_currency_id' => null,
|
||||
'foreign_currency_code' => null,
|
||||
'foreign_currency_symbol' => null,
|
||||
'foreign_currency_name' => null,
|
||||
'foreign_currency_decimal_places' => null,
|
||||
'destination_account_id' => $journal['destination_account_id'],
|
||||
'destination_account_name' => $journal['destination_account_name'],
|
||||
'source_account_id' => $journal['source_account_id'],
|
||||
'source_account_name' => $journal['source_account_name'],
|
||||
'category_name' => $journal['category_name'],
|
||||
'description' => $journal['description'],
|
||||
'transaction_group_id' => $journal['transaction_group_id'],
|
||||
'date' => $journal['date'],
|
||||
];
|
||||
if (null !== $journal['foreign_amount']) {
|
||||
$final['foreign_amount'] = app('steam')->negative($journal['foreign_amount']);
|
||||
$final['foreign_currency_id'] = $journal['foreign_currency_id'];
|
||||
$final['foreign_currency_code'] = $journal['foreign_currency_code'];
|
||||
$final['foreign_currency_symbol'] = $journal['foreign_currency_symbol'];
|
||||
$final['foreign_currency_name'] = $journal['foreign_currency_name'];
|
||||
$final['foreign_currency_decimal_places'] = $journal['foreign_currency_decimal_places'];
|
||||
}
|
||||
|
||||
$array[$currencyId]['budgets'][$budgetId]['transaction_journals'][$journalId] = $final;
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function getBudgets(): Collection
|
||||
{
|
||||
/** @var BudgetRepositoryInterface $repos */
|
||||
$repos = app(BudgetRepositoryInterface::class);
|
||||
$repos->setAdministrationId($this->getAdministrationId());
|
||||
|
||||
return $repos->getActiveBudgets();
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* OperationsRepositoryInterface.php
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Repositories\Administration\Budget;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Interface OperationsRepositoryInterface
|
||||
*/
|
||||
interface OperationsRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* This method returns a list of all the withdrawal transaction journals (as arrays) set in that period
|
||||
* which have the specified budget set to them. It's grouped per currency, with as few details in the array
|
||||
* as possible. Amounts are always negative.
|
||||
*
|
||||
* @param Carbon $start
|
||||
* @param Carbon $end
|
||||
* @param Collection|null $accounts
|
||||
* @param Collection|null $budgets
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function listExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $budgets = null): array;
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* PiggyBankRepository.php
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Repositories\Administration\PiggyBank;
|
||||
|
||||
use FireflyIII\Support\Repositories\Administration\AdministrationTrait;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class PiggyBankRepository
|
||||
*/
|
||||
class PiggyBankRepository implements PiggyBankRepositoryInterface
|
||||
{
|
||||
use AdministrationTrait;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getPiggyBanks(): Collection
|
||||
{
|
||||
return $this->userGroup->piggyBanks()
|
||||
->with(
|
||||
[
|
||||
'account',
|
||||
'objectGroups',
|
||||
]
|
||||
)
|
||||
->orderBy('order', 'ASC')->get();
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* PiggyBankRepositoryInterface.php
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Repositories\Administration\PiggyBank;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Interface PiggyBankRepositoryInterface
|
||||
*/
|
||||
interface PiggyBankRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Return all piggy banks.
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getPiggyBanks(): Collection;
|
||||
}
|
@ -249,6 +249,7 @@ class AvailableBudgetRepository implements AvailableBudgetRepositoryInterface
|
||||
return AvailableBudget::create(
|
||||
[
|
||||
'user_id' => $this->user->id,
|
||||
'user_group_id' => $this->user->user_group_id,
|
||||
'transaction_currency_id' => $data['currency_id'],
|
||||
'amount' => $data['amount'],
|
||||
'start_date' => $start,
|
||||
|
@ -798,10 +798,11 @@ class BudgetRepository implements BudgetRepositoryInterface
|
||||
try {
|
||||
$newBudget = Budget::create(
|
||||
[
|
||||
'user_id' => $this->user->id,
|
||||
'name' => $data['name'],
|
||||
'order' => $order + 1,
|
||||
'active' => array_key_exists('active', $data) ? $data['active'] : true,
|
||||
'user_id' => $this->user->id,
|
||||
'user_group_id' => $this->user->user_group_id,
|
||||
'name' => $data['name'],
|
||||
'order' => $order + 1,
|
||||
'active' => array_key_exists('active', $data) ? $data['active'] : true,
|
||||
]
|
||||
);
|
||||
} catch (QueryException $e) {
|
||||
|
@ -53,9 +53,10 @@ trait CreatesObjectGroups
|
||||
if (!$this->hasObjectGroup($title)) {
|
||||
return ObjectGroup::create(
|
||||
[
|
||||
'user_id' => $this->user->id,
|
||||
'title' => $title,
|
||||
'order' => $maxOrder + 1,
|
||||
'user_id' => $this->user->id,
|
||||
'user_group_id' => $this->user->user_group_id,
|
||||
'title' => $title,
|
||||
'order' => $maxOrder + 1,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -280,7 +280,8 @@ class RuleRepository implements RuleRepositoryInterface
|
||||
|
||||
// start by creating a new rule:
|
||||
$rule = new Rule();
|
||||
$rule->user()->associate($this->user->id);
|
||||
$rule->user()->associate($this->user);
|
||||
$rule->userGroup()->associate($this->user->userGroup);
|
||||
|
||||
$rule->rule_group_id = $ruleGroup->id;
|
||||
$rule->order = 31337;
|
||||
|
@ -121,14 +121,15 @@ class WebhookRepository implements WebhookRepositoryInterface
|
||||
{
|
||||
$secret = Str::random(24);
|
||||
$fullData = [
|
||||
'user_id' => $this->user->id,
|
||||
'active' => $data['active'] ?? false,
|
||||
'title' => $data['title'] ?? null,
|
||||
'trigger' => $data['trigger'],
|
||||
'response' => $data['response'],
|
||||
'delivery' => $data['delivery'],
|
||||
'secret' => $secret,
|
||||
'url' => $data['url'],
|
||||
'user_id' => $this->user->id,
|
||||
'user_group_id' => $this->user->user_group_id,
|
||||
'active' => $data['active'] ?? false,
|
||||
'title' => $data['title'] ?? null,
|
||||
'trigger' => $data['trigger'],
|
||||
'response' => $data['response'],
|
||||
'delivery' => $data['delivery'],
|
||||
'secret' => $secret,
|
||||
'url' => $data['url'],
|
||||
];
|
||||
|
||||
return Webhook::create($fullData);
|
||||
|
@ -182,11 +182,14 @@ class CreditRecalculateService
|
||||
*/
|
||||
private function processWorkAccount(Account $account): void
|
||||
{
|
||||
app('log')->debug(sprintf('Now processing account #%d ("%s")', $account->id, $account->name));
|
||||
// get opening balance (if present)
|
||||
$this->repository->setUser($account->user);
|
||||
$startOfDebt = $this->repository->getOpeningBalanceAmount($account) ?? '0';
|
||||
$leftOfDebt = app('steam')->positive($startOfDebt);
|
||||
|
||||
app('log')->debug(sprintf('Start of debt is "%s", so initial left of debt is "%s"', $startOfDebt, $leftOfDebt));
|
||||
|
||||
/** @var AccountMetaFactory $factory */
|
||||
$factory = app(AccountMetaFactory::class);
|
||||
|
||||
@ -196,13 +199,22 @@ class CreditRecalculateService
|
||||
// get direction of liability:
|
||||
$direction = (string)$this->repository->getMetaValue($account, 'liability_direction');
|
||||
|
||||
app('log')->debug(sprintf('Debt direction is "%s"', $direction));
|
||||
|
||||
// now loop all transactions (except opening balance and credit thing)
|
||||
$transactions = $account->transactions()->get();
|
||||
$transactions = $account->transactions()
|
||||
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||
->orderBy('transaction_journals.date', 'ASC')
|
||||
->get(['transactions.*']);
|
||||
$total = $transactions->count();
|
||||
app('log')->debug(sprintf('Found %d transaction(s) to process.', $total));
|
||||
/** @var Transaction $transaction */
|
||||
foreach ($transactions as $transaction) {
|
||||
foreach ($transactions as $index => $transaction) {
|
||||
app('log')->debug(sprintf('[%d/%d] Processing transaction.', $index + 1, $total));
|
||||
$leftOfDebt = $this->processTransaction($account, $direction, $transaction, $leftOfDebt);
|
||||
}
|
||||
$factory->crud($account, 'current_debt', $leftOfDebt);
|
||||
app('log')->debug(sprintf('Done processing account #%d ("%s")', $account->id, $account->name));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -215,6 +227,7 @@ class CreditRecalculateService
|
||||
*/
|
||||
private function processTransaction(Account $account, string $direction, Transaction $transaction, string $leftOfDebt): string
|
||||
{
|
||||
app('log')->debug(sprintf('Left of debt is: %s', $leftOfDebt));
|
||||
$journal = $transaction->transactionJournal;
|
||||
$foreignCurrency = $transaction->foreignCurrency;
|
||||
$accountCurrency = $this->repository->getAccountCurrency($account);
|
||||
@ -226,16 +239,20 @@ class CreditRecalculateService
|
||||
$sourceTransaction = $journal->transactions()->where('amount', '<', '0')->first();
|
||||
|
||||
if ('' === $direction) {
|
||||
app('log')->warning('Direction is empty, so do nothing.');
|
||||
return $leftOfDebt;
|
||||
}
|
||||
if (TransactionType::LIABILITY_CREDIT === $type || TransactionType::OPENING_BALANCE === $type) {
|
||||
app('log')->warning(sprintf('Transaction type is "%s", so do nothing.', $type));
|
||||
return $leftOfDebt;
|
||||
}
|
||||
|
||||
// amount to use depends on the currency:
|
||||
$usedAmount = $transaction->amount;
|
||||
app('log')->debug(sprintf('Amount of transaction is %s', $usedAmount));
|
||||
if (null !== $foreignCurrency && $foreignCurrency->id === $accountCurrency->id) {
|
||||
$usedAmount = $transaction->foreign_amount;
|
||||
app('log')->debug(sprintf('Overruled by foreign amount. Amount of transaction is now %s', $usedAmount));
|
||||
}
|
||||
|
||||
// Case 1
|
||||
@ -248,7 +265,10 @@ class CreditRecalculateService
|
||||
&& 1 === bccomp($usedAmount, '0')
|
||||
&& 'credit' === $direction
|
||||
) {
|
||||
return bcadd($leftOfDebt, app('steam')->positive($usedAmount));
|
||||
$usedAmount = app('steam')->positive($usedAmount);
|
||||
$result = bcadd($leftOfDebt, $usedAmount);
|
||||
app('log')->debug(sprintf('Case 1 (withdrawal into credit liability): %s + %s = %s', $leftOfDebt, $usedAmount, $result));
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Case 2
|
||||
@ -261,7 +281,10 @@ class CreditRecalculateService
|
||||
&& -1 === bccomp($usedAmount, '0')
|
||||
&& 'credit' === $direction
|
||||
) {
|
||||
return bcsub($leftOfDebt, app('steam')->positive($usedAmount));
|
||||
$usedAmount = app('steam')->positive($usedAmount);
|
||||
$result = bcsub($leftOfDebt, $usedAmount);
|
||||
app('log')->debug(sprintf('Case 2 (withdrawal away from liability): %s - %s = %s', $leftOfDebt, $usedAmount, $result));
|
||||
return $result;
|
||||
}
|
||||
|
||||
// case 3
|
||||
@ -274,7 +297,10 @@ class CreditRecalculateService
|
||||
&& -1 === bccomp($usedAmount, '0')
|
||||
&& 'credit' === $direction
|
||||
) {
|
||||
return bcsub($leftOfDebt, app('steam')->positive($usedAmount));
|
||||
$usedAmount = app('steam')->positive($usedAmount);
|
||||
$result = bcsub($leftOfDebt, $usedAmount);
|
||||
app('log')->debug(sprintf('Case 3 (deposit away from liability): %s - %s = %s', $leftOfDebt, $usedAmount, $result));
|
||||
return $result;
|
||||
}
|
||||
|
||||
// case 4
|
||||
@ -287,14 +313,32 @@ class CreditRecalculateService
|
||||
&& 1 === bccomp($usedAmount, '0')
|
||||
&& 'credit' === $direction
|
||||
) {
|
||||
$newLeftOfDebt = bcadd($leftOfDebt, app('steam')->positive($usedAmount));
|
||||
return $newLeftOfDebt;
|
||||
$usedAmount = app('steam')->positive($usedAmount);
|
||||
$result = bcadd($leftOfDebt, $usedAmount);
|
||||
app('log')->debug(sprintf('Case 4 (deposit into credit liability): %s + %s = %s', $leftOfDebt, $usedAmount, $result));
|
||||
return $result;
|
||||
}
|
||||
// case 5: transfer into loan (from other loan).
|
||||
// if it's a credit ("I am owed") this increases the amount due,
|
||||
// because the person has to pay more back.
|
||||
if (
|
||||
$type === TransactionType::TRANSFER
|
||||
&& (int)$account->id === (int)$destTransaction->account_id
|
||||
&& 1 === bccomp($usedAmount, '0')
|
||||
&& 'credit' === $direction
|
||||
) {
|
||||
$usedAmount = app('steam')->positive($usedAmount);
|
||||
$result = bcadd($leftOfDebt, $usedAmount);
|
||||
app('log')->debug(sprintf('Case 5 (transfer into credit liability): %s + %s = %s', $leftOfDebt, $usedAmount, $result));
|
||||
return $result;
|
||||
}
|
||||
|
||||
// in any other case, remove amount from left of debt.
|
||||
if (in_array($type, [TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::TRANSFER], true)) {
|
||||
$newLeftOfDebt = bcadd($leftOfDebt, bcmul($usedAmount, '-1'));
|
||||
return $newLeftOfDebt;
|
||||
$usedAmount = app('steam')->negative($usedAmount);
|
||||
$result = bcadd($leftOfDebt, $usedAmount);
|
||||
app('log')->debug(sprintf('Case X (all other cases): %s + %s = %s', $leftOfDebt, $usedAmount, $result));
|
||||
return $result;
|
||||
}
|
||||
|
||||
Log::warning(sprintf('[6] Catch-all, should not happen. Left of debt = %s', $leftOfDebt));
|
||||
|
@ -493,10 +493,11 @@ trait JournalServiceTrait
|
||||
if ('' !== $string) {
|
||||
$tag = $this->tagFactory->findOrCreate($string);
|
||||
if (null !== $tag) {
|
||||
$set[] = $tag->id;
|
||||
$set[] = (int)$tag->id;
|
||||
}
|
||||
}
|
||||
}
|
||||
$set = array_unique($set);
|
||||
Log::debug('End of loop.');
|
||||
Log::debug(sprintf('Total nr. of tags: %d', count($tags)), $tags);
|
||||
$journal->tags()->sync($set);
|
||||
|
@ -143,9 +143,8 @@ class JournalUpdateService
|
||||
Log::debug(sprintf('Now in %s', __METHOD__));
|
||||
Log::debug(sprintf('Now in JournalUpdateService for journal #%d.', $this->transactionJournal->id));
|
||||
|
||||
if ($this->removeReconciliation()) {
|
||||
$this->data['reconciled'] = false;
|
||||
}
|
||||
|
||||
$this->data['reconciled'] = array_key_exists('reconciled', $this->data) ? $this->data['reconciled'] : false;
|
||||
|
||||
// can we update account data using the new type?
|
||||
if ($this->hasValidAccounts()) {
|
||||
@ -182,21 +181,6 @@ class JournalUpdateService
|
||||
$this->transactionJournal->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
private function removeReconciliation(): bool
|
||||
{
|
||||
if (count($this->data) > 1) {
|
||||
return true;
|
||||
}
|
||||
if (1 === count($this->data) && true === array_key_exists('transaction_journal_id', $this->data)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
|
59
app/Support/Binder/UserGroupAccount.php
Normal file
59
app/Support/Binder/UserGroupAccount.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* AccountList.php
|
||||
* Copyright (c) 2019 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Support\Binder;
|
||||
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Collection;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* Class UserGroupAccount.
|
||||
*/
|
||||
class UserGroupAccount implements BinderInterface
|
||||
{
|
||||
/**
|
||||
* @param string $value
|
||||
* @param Route $route
|
||||
*
|
||||
* @return Account
|
||||
* @throws NotFoundHttpException
|
||||
*
|
||||
*/
|
||||
public static function routeBinder(string $value, Route $route): Account
|
||||
{
|
||||
if (auth()->check()) {
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
$currency = Account::where('id', (int)$value)
|
||||
->where('user_group_id', $user->user_group_id)
|
||||
->first();
|
||||
if (null !== $currency) {
|
||||
return $currency;
|
||||
}
|
||||
}
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
}
|
@ -64,9 +64,9 @@ class RuleForm
|
||||
* @param null $value
|
||||
* @param array|null $options
|
||||
*
|
||||
* @return HtmlString
|
||||
* @return string
|
||||
*/
|
||||
public function ruleGroupListWithEmpty(string $name, $value = null, array $options = null): HtmlString
|
||||
public function ruleGroupListWithEmpty(string $name, $value = null, array $options = null): string
|
||||
{
|
||||
$options = $options ?? [];
|
||||
$options['class'] = 'form-control';
|
||||
@ -85,6 +85,6 @@ class RuleForm
|
||||
}
|
||||
}
|
||||
|
||||
return Form::select($name, $array, $value, $options);
|
||||
return $this->select($name, $array, $value, $options);
|
||||
}
|
||||
}
|
||||
|
71
app/Support/Http/Api/CleansChartData.php
Normal file
71
app/Support/Http/Api/CleansChartData.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/*
|
||||
* CleansChartData.php
|
||||
* Copyright (c) 2023 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Support\Http\Api;
|
||||
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
|
||||
/**
|
||||
* Trait CleansChartData
|
||||
*/
|
||||
trait CleansChartData
|
||||
{
|
||||
/**
|
||||
* Clean up given chart data array. Each entry is supposed to be a
|
||||
* "main" entry used in the V2 API chart endpoints. This loop makes sure
|
||||
* IDs are strings and other values are present (or missing).
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return array
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function clean(array $data): array
|
||||
{
|
||||
$return = [];
|
||||
/**
|
||||
* @var mixed $index
|
||||
* @var array $array
|
||||
*/
|
||||
foreach ($data as $index => $array) {
|
||||
if (array_key_exists('currency_id', $array)) {
|
||||
$array['currency_id'] = (string)$array['currency_id'];
|
||||
}
|
||||
if (array_key_exists('native_id', $array)) {
|
||||
$array['native_id'] = (string)$array['native_id'];
|
||||
}
|
||||
if (!array_key_exists('start', $array)) {
|
||||
throw new FireflyException(sprintf('Data-set "%s" is missing the "start"-variable.', $index));
|
||||
}
|
||||
if (!array_key_exists('end', $array)) {
|
||||
throw new FireflyException(sprintf('Data-set "%s" is missing the "end"-variable.', $index));
|
||||
}
|
||||
if (!array_key_exists('period', $array)) {
|
||||
throw new FireflyException(sprintf('Data-set "%s" is missing the "period"-variable.', $index));
|
||||
}
|
||||
$return[] = $array;
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
}
|
@ -42,6 +42,7 @@ trait ConvertsExchangeRates
|
||||
* @param array $set
|
||||
*
|
||||
* @return array
|
||||
* @deprecated
|
||||
*/
|
||||
public function cerChartSet(array $set): array
|
||||
{
|
||||
@ -80,6 +81,7 @@ trait ConvertsExchangeRates
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @deprecated
|
||||
*/
|
||||
private function getPreference(): void
|
||||
{
|
||||
@ -90,6 +92,7 @@ trait ConvertsExchangeRates
|
||||
* @param int $currencyId
|
||||
*
|
||||
* @return TransactionCurrency
|
||||
* @deprecated
|
||||
*/
|
||||
private function getCurrency(int $currencyId): TransactionCurrency
|
||||
{
|
||||
@ -100,128 +103,6 @@ trait ConvertsExchangeRates
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TransactionCurrency $from
|
||||
* @param TransactionCurrency $to
|
||||
* @param Carbon $date
|
||||
*
|
||||
* @return string
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function getRate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string
|
||||
{
|
||||
// first attempt:
|
||||
$rate = $this->getFromDB((int)$from->id, (int)$to->id, $date->format('Y-m-d'));
|
||||
if (null !== $rate) {
|
||||
return $rate;
|
||||
}
|
||||
// no result. perhaps the other way around?
|
||||
$rate = $this->getFromDB((int)$to->id, (int)$from->id, $date->format('Y-m-d'));
|
||||
if (null !== $rate) {
|
||||
return bcdiv('1', $rate);
|
||||
}
|
||||
|
||||
// if nothing in place, fall back on the rate for $from to EUR
|
||||
$first = $this->getEuroRate($from, $date);
|
||||
$second = $this->getEuroRate($to, $date);
|
||||
|
||||
// combined (if present), they can be used to calculate the necessary conversion rate.
|
||||
if ('0' === $first || '0' === $second) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
$second = bcdiv('1', $second);
|
||||
return bcmul($first, $second);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $from
|
||||
* @param int $to
|
||||
* @param string $date
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function getFromDB(int $from, int $to, string $date): ?string
|
||||
{
|
||||
$key = sprintf('cer-%d-%d-%s', $from, $to, $date);
|
||||
|
||||
$cache = new CacheProperties();
|
||||
$cache->addProperty($key);
|
||||
if ($cache->has()) {
|
||||
return $cache->get();
|
||||
}
|
||||
|
||||
/** @var CurrencyExchangeRate $result */
|
||||
$result = auth()->user()
|
||||
->currencyExchangeRates()
|
||||
->where('from_currency_id', $from)
|
||||
->where('to_currency_id', $to)
|
||||
->where('date', '<=', $date)
|
||||
->orderBy('date', 'DESC')
|
||||
->first();
|
||||
if (null !== $result) {
|
||||
$rate = (string)$result->rate;
|
||||
$cache->store($rate);
|
||||
return $rate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TransactionCurrency $currency
|
||||
* @param Carbon $date
|
||||
*
|
||||
* @return string
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function getEuroRate(TransactionCurrency $currency, Carbon $date): string
|
||||
{
|
||||
$euroId = $this->getEuroId();
|
||||
if ($euroId === (int)$currency->id) {
|
||||
return '1';
|
||||
}
|
||||
$rate = $this->getFromDB((int)$currency->id, $euroId, $date->format('Y-m-d'));
|
||||
|
||||
if (null !== $rate) {
|
||||
// app('log')->debug(sprintf('Rate for %s to EUR is %s.', $currency->code, $rate));
|
||||
return $rate;
|
||||
}
|
||||
$rate = $this->getFromDB($euroId, (int)$currency->id, $date->format('Y-m-d'));
|
||||
if (null !== $rate) {
|
||||
$rate = bcdiv('1', $rate);
|
||||
// app('log')->debug(sprintf('Inverted rate for %s to EUR is %s.', $currency->code, $rate));
|
||||
return $rate;
|
||||
}
|
||||
// grab backup values from config file:
|
||||
$backup = config(sprintf('cer.rates.%s', $currency->code));
|
||||
if (null !== $backup) {
|
||||
$backup = bcdiv('1', (string)$backup);
|
||||
// app('log')->debug(sprintf('Backup rate for %s to EUR is %s.', $currency->code, $backup));
|
||||
return $backup;
|
||||
}
|
||||
|
||||
// app('log')->debug(sprintf('No rate for %s to EUR.', $currency->code));
|
||||
return '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function getEuroId(): int
|
||||
{
|
||||
$cache = new CacheProperties();
|
||||
$cache->addProperty('cer-euro-id');
|
||||
if ($cache->has()) {
|
||||
return $cache->get();
|
||||
}
|
||||
$euro = TransactionCurrency::whereCode('EUR')->first();
|
||||
if (null === $euro) {
|
||||
throw new FireflyException('Cannot find EUR in system, cannot do currency conversion.');
|
||||
}
|
||||
$cache->store((int)$euro->id);
|
||||
return (int)$euro->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a sum of entries, get the exchange rate to the native currency of
|
||||
@ -230,9 +111,11 @@ trait ConvertsExchangeRates
|
||||
* @param array $entries
|
||||
*
|
||||
* @return array
|
||||
* @deprecated
|
||||
*/
|
||||
public function cerSum(array $entries): array
|
||||
{
|
||||
die('do not use me, needs refactor');
|
||||
if (null === $this->enabled) {
|
||||
$this->getPreference();
|
||||
}
|
||||
@ -286,6 +169,8 @@ trait ConvertsExchangeRates
|
||||
* @param Carbon|null $date
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
private function convertAmount(string $amount, TransactionCurrency $from, TransactionCurrency $to, ?Carbon $date = null): string
|
||||
{
|
||||
|
@ -25,14 +25,31 @@ namespace FireflyIII\Support\Http\Api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Models\CurrencyExchangeRate;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Support\CacheProperties;
|
||||
|
||||
/**
|
||||
* Class ExchangeRateConverter
|
||||
*/
|
||||
class ExchangeRateConverter
|
||||
{
|
||||
use ConvertsExchangeRates;
|
||||
//use ConvertsExchangeRates;
|
||||
|
||||
/**
|
||||
* @param TransactionCurrency $from
|
||||
* @param TransactionCurrency $to
|
||||
* @param Carbon $date
|
||||
* @param string $amount
|
||||
*
|
||||
* @return string
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function convert(TransactionCurrency $from, TransactionCurrency $to, Carbon $date, string $amount): string
|
||||
{
|
||||
$rate = $this->getCurrencyRate($from, $to, $date);
|
||||
return bcmul($amount, $rate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TransactionCurrency $from
|
||||
@ -44,18 +61,138 @@ class ExchangeRateConverter
|
||||
*/
|
||||
public function getCurrencyRate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string
|
||||
{
|
||||
if (null === $this->enabled) {
|
||||
$this->getPreference();
|
||||
}
|
||||
|
||||
// if not enabled, return "1"
|
||||
if (false === $this->enabled) {
|
||||
return '1';
|
||||
}
|
||||
|
||||
$rate = $this->getRate($from, $to, $date);
|
||||
return '0' === $rate ? '1' : $rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TransactionCurrency $from
|
||||
* @param TransactionCurrency $to
|
||||
* @param Carbon $date
|
||||
*
|
||||
* @return string
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function getRate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string
|
||||
{
|
||||
// first attempt:
|
||||
$rate = $this->getFromDB((int)$from->id, (int)$to->id, $date->format('Y-m-d'));
|
||||
if (null !== $rate) {
|
||||
return $rate;
|
||||
}
|
||||
// no result. perhaps the other way around?
|
||||
$rate = $this->getFromDB((int)$to->id, (int)$from->id, $date->format('Y-m-d'));
|
||||
if (null !== $rate) {
|
||||
return bcdiv('1', $rate);
|
||||
}
|
||||
|
||||
// if nothing in place, fall back on the rate for $from to EUR
|
||||
$first = $this->getEuroRate($from, $date);
|
||||
$second = $this->getEuroRate($to, $date);
|
||||
|
||||
// combined (if present), they can be used to calculate the necessary conversion rate.
|
||||
if ('0' === $first || '0' === $second) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
$second = bcdiv('1', $second);
|
||||
return bcmul($first, $second);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $from
|
||||
* @param int $to
|
||||
* @param string $date
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function getFromDB(int $from, int $to, string $date): ?string
|
||||
{
|
||||
$key = sprintf('cer-%d-%d-%s', $from, $to, $date);
|
||||
|
||||
$cache = new CacheProperties();
|
||||
$cache->addProperty($key);
|
||||
if ($cache->has()) {
|
||||
$rate = $cache->get();
|
||||
if ('' === $rate) {
|
||||
return null;
|
||||
}
|
||||
return $rate;
|
||||
}
|
||||
app('log')->debug(sprintf('Going to get rate #%d->#%d (%s) from DB.', $from, $to, $date));
|
||||
|
||||
/** @var CurrencyExchangeRate $result */
|
||||
$result = auth()->user()
|
||||
->currencyExchangeRates()
|
||||
->where('from_currency_id', $from)
|
||||
->where('to_currency_id', $to)
|
||||
->where('date', '<=', $date)
|
||||
->orderBy('date', 'DESC')
|
||||
->first();
|
||||
$rate = (string)$result?->rate;
|
||||
$cache->store($rate);
|
||||
if ('' === $rate) {
|
||||
return null;
|
||||
}
|
||||
return $rate;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param TransactionCurrency $currency
|
||||
* @param Carbon $date
|
||||
*
|
||||
* @return string
|
||||
* @throws FireflyException
|
||||
*
|
||||
*/
|
||||
private function getEuroRate(TransactionCurrency $currency, Carbon $date): string
|
||||
{
|
||||
$euroId = $this->getEuroId();
|
||||
if ($euroId === (int)$currency->id) {
|
||||
return '1';
|
||||
}
|
||||
$rate = $this->getFromDB((int)$currency->id, $euroId, $date->format('Y-m-d'));
|
||||
|
||||
if (null !== $rate) {
|
||||
// app('log')->debug(sprintf('Rate for %s to EUR is %s.', $currency->code, $rate));
|
||||
return $rate;
|
||||
}
|
||||
$rate = $this->getFromDB($euroId, (int)$currency->id, $date->format('Y-m-d'));
|
||||
if (null !== $rate) {
|
||||
return bcdiv('1', $rate);
|
||||
// app('log')->debug(sprintf('Inverted rate for %s to EUR is %s.', $currency->code, $rate));
|
||||
//return $rate;
|
||||
}
|
||||
// grab backup values from config file:
|
||||
$backup = config(sprintf('cer.rates.%s', $currency->code));
|
||||
if (null !== $backup) {
|
||||
return bcdiv('1', (string)$backup);
|
||||
// app('log')->debug(sprintf('Backup rate for %s to EUR is %s.', $currency->code, $backup));
|
||||
//return $backup;
|
||||
}
|
||||
|
||||
// app('log')->debug(sprintf('No rate for %s to EUR.', $currency->code));
|
||||
return '0';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return int
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function getEuroId(): int
|
||||
{
|
||||
$cache = new CacheProperties();
|
||||
$cache->addProperty('cer-euro-id');
|
||||
if ($cache->has()) {
|
||||
return (int)$cache->get();
|
||||
}
|
||||
$euro = TransactionCurrency::whereCode('EUR')->first();
|
||||
if (null === $euro) {
|
||||
throw new FireflyException('Cannot find EUR in system, cannot do currency conversion.');
|
||||
}
|
||||
$cache->store((int)$euro->id);
|
||||
return (int)$euro->id;
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ trait TransactionFilter
|
||||
*/
|
||||
protected function mapTransactionTypes(string $type): array
|
||||
{
|
||||
$types = [
|
||||
$types = [
|
||||
'all' => [
|
||||
TransactionType::WITHDRAWAL,
|
||||
TransactionType::DEPOSIT,
|
||||
@ -65,7 +65,11 @@ trait TransactionFilter
|
||||
'specials' => [TransactionType::OPENING_BALANCE, TransactionType::RECONCILIATION,],
|
||||
'default' => [TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::TRANSFER,],
|
||||
];
|
||||
|
||||
return $types[$type] ?? $types['default'];
|
||||
$return = [];
|
||||
$parts = explode(',', $type);
|
||||
foreach ($parts as $part) {
|
||||
$return = array_merge($return, $types[$part] ?? $types['default']);
|
||||
}
|
||||
return array_unique($return);
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ trait RuleManagement
|
||||
]
|
||||
)->render();
|
||||
} catch (Throwable $e) {
|
||||
Log::debug(sprintf('Throwable was thrown in getPreviousActions(): %s', $e->getMessage()));
|
||||
Log::error(sprintf('Throwable was thrown in getPreviousActions(): %s', $e->getMessage()));
|
||||
Log::error($e->getTraceAsString());
|
||||
throw new FireflyException('Could not render', 0, $e);
|
||||
}
|
||||
|
@ -515,6 +515,25 @@ class Navigation
|
||||
return $date->format('Y-m-d');
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as preferredCarbonFormat but by string
|
||||
*
|
||||
* @param string $period
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function preferredCarbonFormatByPeriod(string $period): string
|
||||
{
|
||||
return match ($period) {
|
||||
default => 'Y-m-d',
|
||||
//'1D' => 'Y-m-d',
|
||||
'1W' => '\WW,Y',
|
||||
'1M' => 'Y-m',
|
||||
'3M', '6M' => '\QQ,Y',
|
||||
'1Y' => 'Y',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* If the date difference between start and end is less than a month, method returns trans(config.month_and_day).
|
||||
* If the difference is less than a year, method returns "config.month". If the date difference is larger, method
|
||||
|
@ -26,6 +26,8 @@ namespace FireflyIII\Support\Request;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\Exceptions\InvalidDateException;
|
||||
use Carbon\Exceptions\InvalidFormatException;
|
||||
use FireflyIII\Repositories\Administration\Account\AccountRepositoryInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
@ -153,6 +155,38 @@ trait ConvertsDataTypes
|
||||
return trim($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO duplicate, see SelectTransactionsRequest
|
||||
*
|
||||
* Validate list of accounts. This one is for V2 endpoints, so it searches for groups, not users.
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getAccountList(): Collection
|
||||
{
|
||||
// fixed
|
||||
/** @var AccountRepositoryInterface $repository */
|
||||
$repository = app(AccountRepositoryInterface::class);
|
||||
|
||||
// set administration ID
|
||||
// group ID
|
||||
$administrationId = auth()->user()->getAdministrationId();
|
||||
$repository->setAdministrationId($administrationId);
|
||||
|
||||
$set = $this->get('accounts');
|
||||
$collection = new Collection();
|
||||
if (is_array($set)) {
|
||||
foreach ($set as $accountId) {
|
||||
$account = $repository->find((int)$accountId);
|
||||
if (null !== $account) {
|
||||
$collection->push($account);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return string value with newlines.
|
||||
*
|
||||
@ -342,7 +376,7 @@ trait ConvertsDataTypes
|
||||
{
|
||||
$result = null;
|
||||
try {
|
||||
$result = $this->get($field) ? new Carbon($this->get($field)) : null;
|
||||
$result = $this->get($field) ? new Carbon($this->get($field), config('app.timezone')) : null;
|
||||
} catch (InvalidFormatException $e) {
|
||||
// @ignoreException
|
||||
}
|
||||
|
@ -360,9 +360,9 @@ class Steam
|
||||
$cache->addProperty($account->id);
|
||||
$cache->addProperty('balance');
|
||||
$cache->addProperty($date);
|
||||
$cache->addProperty($native ? $native->id : 0);
|
||||
$cache->addProperty($native->id);
|
||||
if ($cache->has()) {
|
||||
return $cache->get();
|
||||
// return $cache->get();
|
||||
}
|
||||
/** @var AccountRepositoryInterface $repository */
|
||||
$repository = app(AccountRepositoryInterface::class);
|
||||
@ -370,7 +370,9 @@ class Steam
|
||||
if (null === $currency) {
|
||||
throw new FireflyException('Cannot get converted account balance: no currency found for account.');
|
||||
}
|
||||
|
||||
if ((int)$native->id === (int)$currency->id) {
|
||||
return $this->balance($account, $date);
|
||||
}
|
||||
/**
|
||||
* selection of transactions
|
||||
* 1: all normal transactions. No foreign currency info. In $currency. Need conversion.
|
||||
@ -392,7 +394,7 @@ class Steam
|
||||
->where('transactions.transaction_currency_id', $currency->id)
|
||||
->whereNull('transactions.foreign_currency_id')
|
||||
->get(['transaction_journals.date', 'transactions.amount'])->toArray();
|
||||
app('log')->debug(sprintf('%d transactions in set #1', count($new[0])));
|
||||
app('log')->debug(sprintf('%d transaction(s) in set #1', count($new[0])));
|
||||
// 2
|
||||
$existing[] = $account->transactions()
|
||||
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||
@ -400,7 +402,7 @@ class Steam
|
||||
->where('transactions.transaction_currency_id', $native->id)
|
||||
->whereNull('transactions.foreign_currency_id')
|
||||
->get(['transactions.amount'])->toArray();
|
||||
app('log')->debug(sprintf('%d transactions in set #2', count($existing[0])));
|
||||
app('log')->debug(sprintf('%d transaction(s) in set #2', count($existing[0])));
|
||||
// 3
|
||||
$new[] = $account->transactions()
|
||||
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||
@ -464,8 +466,9 @@ class Steam
|
||||
//app('log')->debug(sprintf('Balance from new set #%d is %f', $index, $balance));
|
||||
}
|
||||
|
||||
// add virtual balance
|
||||
// add virtual balance (also needs conversion)
|
||||
$virtual = null === $account->virtual_balance ? '0' : (string)$account->virtual_balance;
|
||||
$virtual = $converter->convert($currency, $native, $account->created_at, $virtual);
|
||||
$balance = bcadd($balance, $virtual);
|
||||
|
||||
$cache->store($balance);
|
||||
@ -506,6 +509,45 @@ class Steam
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method always ignores the virtual balance.
|
||||
*
|
||||
* @param Collection $accounts
|
||||
* @param Carbon $date
|
||||
*
|
||||
* @return array
|
||||
* @throws FireflyException
|
||||
*/
|
||||
public function balancesByAccountsConverted(Collection $accounts, Carbon $date): array
|
||||
{
|
||||
$ids = $accounts->pluck('id')->toArray();
|
||||
// cache this property.
|
||||
$cache = new CacheProperties();
|
||||
$cache->addProperty($ids);
|
||||
$cache->addProperty('balances-converted');
|
||||
$cache->addProperty($date);
|
||||
if ($cache->has()) {
|
||||
// return $cache->get();
|
||||
}
|
||||
|
||||
|
||||
// need to do this per account.
|
||||
$result = [];
|
||||
/** @var Account $account */
|
||||
foreach ($accounts as $account) {
|
||||
$default = app('amount')->getDefaultCurrencyByUser($account->user);
|
||||
$result[(int)$account->id]
|
||||
= [
|
||||
'balance' => $this->balance($account, $date),
|
||||
'native_balance' => $this->balanceConverted($account, $date, $default),
|
||||
];
|
||||
}
|
||||
|
||||
$cache->store($result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as above, but also groups per currency.
|
||||
*
|
||||
|
97
app/TransactionRules/Actions/SwitchAccounts.php
Normal file
97
app/TransactionRules/Actions/SwitchAccounts.php
Normal file
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
/**
|
||||
* ConvertToWithdrawal.php
|
||||
* Copyright (c) 2019 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\TransactionRules\Actions;
|
||||
|
||||
use DB;
|
||||
use FireflyIII\Events\TriggeredAuditLog;
|
||||
use FireflyIII\Models\RuleAction;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
*
|
||||
* Class SwitchAccounts
|
||||
*/
|
||||
class SwitchAccounts implements ActionInterface
|
||||
{
|
||||
private RuleAction $action;
|
||||
|
||||
/**
|
||||
* TriggerInterface constructor.
|
||||
*
|
||||
* @param RuleAction $action
|
||||
*/
|
||||
public function __construct(RuleAction $action)
|
||||
{
|
||||
$this->action = $action;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function actOnArray(array $journal): bool
|
||||
{
|
||||
// make object from array (so the data is fresh).
|
||||
/** @var TransactionJournal|null $object */
|
||||
$object = TransactionJournal::where('user_id', $journal['user_id'])->find($journal['transaction_journal_id']);
|
||||
if (null === $object) {
|
||||
Log::error(sprintf('Cannot find journal #%d, cannot switch accounts.', $journal['transaction_journal_id']));
|
||||
return false;
|
||||
}
|
||||
$groupCount = TransactionJournal::where('transaction_group_id', $journal['transaction_group_id'])->count();
|
||||
if ($groupCount > 1) {
|
||||
Log::error(sprintf('Group #%d has more than one transaction in it, cannot switch accounts.', $journal['transaction_group_id']));
|
||||
return false;
|
||||
}
|
||||
|
||||
$type = $object->transactionType->type;
|
||||
if (TransactionType::TRANSFER !== $type) {
|
||||
Log::error(sprintf('Journal #%d is NOT a transfer (rule #%d), cannot switch accounts.', $journal['transaction_journal_id'], $this->action->rule_id));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var Transaction $sourceTransaction */
|
||||
$sourceTransaction = $object->transactions()->where('amount', '<', 0)->first();
|
||||
/** @var Transaction $destTransaction */
|
||||
$destTransaction = $object->transactions()->where('amount', '>', 0)->first();
|
||||
if (null === $sourceTransaction || null === $destTransaction) {
|
||||
Log::error(sprintf('Journal #%d has no source or destination transaction (rule #%d), cannot switch accounts.', $journal['transaction_journal_id'], $this->action->rule_id));
|
||||
|
||||
return false;
|
||||
}
|
||||
$sourceAccountId = (int)$sourceTransaction->account_id;
|
||||
$destinationAccountId = $destTransaction->account_id;
|
||||
$sourceTransaction->account_id = $destinationAccountId;
|
||||
$destTransaction->account_id = $sourceAccountId;
|
||||
$sourceTransaction->save();
|
||||
$destTransaction->save();
|
||||
|
||||
event(new TriggeredAuditLog($this->action->rule, $object, 'switch_accounts', $sourceAccountId, $destinationAccountId));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -36,6 +36,8 @@ abstract class AbstractTransformer extends TransformerAbstract
|
||||
protected ParameterBag $parameters;
|
||||
|
||||
/**
|
||||
* This method is called exactly ONCE from FireflyIII\Api\V2\Controllers\Controller::jsonApiList
|
||||
*
|
||||
* @param Collection $objects
|
||||
*
|
||||
* @return void
|
||||
|
@ -37,10 +37,11 @@ use Illuminate\Support\Collection;
|
||||
*/
|
||||
class AccountTransformer extends AbstractTransformer
|
||||
{
|
||||
private array $accountMeta;
|
||||
private array $balances;
|
||||
private array $currencies;
|
||||
private ?TransactionCurrency $currency;
|
||||
private array $accountMeta;
|
||||
private array $balances;
|
||||
private array $convertedBalances;
|
||||
private array $currencies;
|
||||
private TransactionCurrency $default;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
@ -48,12 +49,12 @@ class AccountTransformer extends AbstractTransformer
|
||||
*/
|
||||
public function collectMetaData(Collection $objects): void
|
||||
{
|
||||
$this->currency = null;
|
||||
$this->currencies = [];
|
||||
$this->accountMeta = [];
|
||||
$this->balances = app('steam')->balancesByAccounts($objects, $this->getDate());
|
||||
$repository = app(CurrencyRepositoryInterface::class);
|
||||
$this->currency = app('amount')->getDefaultCurrency();
|
||||
$this->currencies = [];
|
||||
$this->accountMeta = [];
|
||||
$this->balances = app('steam')->balancesByAccounts($objects, $this->getDate());
|
||||
$this->convertedBalances = app('steam')->balancesByAccountsConverted($objects, $this->getDate());
|
||||
$repository = app(CurrencyRepositoryInterface::class);
|
||||
$this->default = app('amount')->getDefaultCurrency();
|
||||
|
||||
// get currencies:
|
||||
$accountIds = $objects->pluck('id')->toArray();
|
||||
@ -100,10 +101,13 @@ class AccountTransformer extends AbstractTransformer
|
||||
$id = (int)$account->id;
|
||||
|
||||
// no currency? use default
|
||||
$currency = $this->currency;
|
||||
$currency = $this->default;
|
||||
if (0 !== (int)$this->accountMeta[$id]['currency_id']) {
|
||||
$currency = $this->currencies[(int)$this->accountMeta[$id]['currency_id']];
|
||||
}
|
||||
// amounts and calculation.
|
||||
$balance = $this->balances[$id] ?? null;
|
||||
$nativeBalance = $this->convertedBalances[$id]['native_balance'] ?? null;
|
||||
|
||||
return [
|
||||
'id' => (string)$account->id,
|
||||
@ -112,19 +116,30 @@ class AccountTransformer extends AbstractTransformer
|
||||
'active' => $account->active,
|
||||
//'order' => $order,
|
||||
'name' => $account->name,
|
||||
'iban' => '' === $account->iban ? null : $account->iban,
|
||||
// 'type' => strtolower($accountType),
|
||||
// 'account_role' => $accountRole,
|
||||
'currency_id' => $currency->id,
|
||||
'currency_id' => (string)$currency->id,
|
||||
'currency_code' => $currency->code,
|
||||
'currency_symbol' => $currency->symbol,
|
||||
'currency_decimal_places' => $currency->decimal_places,
|
||||
'current_balance' => $this->balances[$id] ?? null,
|
||||
'current_balance_date' => $this->getDate(),
|
||||
'currency_decimal_places' => (int)$currency->decimal_places,
|
||||
|
||||
'native_id' => (string)$this->default->id,
|
||||
'native_code' => $this->default->code,
|
||||
'native_symbol' => $this->default->symbol,
|
||||
'native_decimal_places' => (int)$this->default->decimal_places,
|
||||
|
||||
// balance:
|
||||
'current_balance' => $balance,
|
||||
'native_current_balance' => $nativeBalance,
|
||||
'current_balance_date' => $this->getDate(),
|
||||
|
||||
// more meta
|
||||
|
||||
// 'notes' => $this->repository->getNoteText($account),
|
||||
// 'monthly_payment_date' => $monthlyPaymentDate,
|
||||
// 'credit_card_type' => $creditCardType,
|
||||
// 'account_number' => $this->repository->getMetaValue($account, 'account_number'),
|
||||
'iban' => '' === $account->iban ? null : $account->iban,
|
||||
// 'bic' => $this->repository->getMetaValue($account, 'BIC'),
|
||||
// 'virtual_balance' => number_format((float) $account->virtual_balance, $decimalPlaces, '.', ''),
|
||||
// 'opening_balance' => $openingBalance,
|
||||
@ -138,7 +153,7 @@ class AccountTransformer extends AbstractTransformer
|
||||
// 'longitude' => $longitude,
|
||||
// 'latitude' => $latitude,
|
||||
// 'zoom_level' => $zoomLevel,
|
||||
'links' => [
|
||||
'links' => [
|
||||
[
|
||||
'rel' => 'self',
|
||||
'uri' => '/accounts/' . $account->id,
|
||||
|
320
app/Transformers/V2/BillTransformer.php
Normal file
320
app/Transformers/V2/BillTransformer.php
Normal file
@ -0,0 +1,320 @@
|
||||
<?php
|
||||
/**
|
||||
* BillTransformer.php
|
||||
* Copyright (c) 2019 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Transformers\V2;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonInterface;
|
||||
use FireflyIII\Models\Bill;
|
||||
use FireflyIII\Models\Note;
|
||||
use FireflyIII\Models\ObjectGroup;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Log;
|
||||
|
||||
/**
|
||||
* Class BillTransformer
|
||||
*/
|
||||
class BillTransformer extends AbstractTransformer
|
||||
{
|
||||
private array $currencies;
|
||||
private TransactionCurrency $default;
|
||||
private array $groups;
|
||||
private array $notes;
|
||||
private array $paidDates;
|
||||
private BillRepositoryInterface $repository;
|
||||
|
||||
/**
|
||||
* BillTransformer constructor.
|
||||
*
|
||||
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->repository = app(BillRepositoryInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function collectMetaData(Collection $objects): void
|
||||
{
|
||||
$currencies = [];
|
||||
$bills = [];
|
||||
$this->notes = [];
|
||||
$this->groups = [];
|
||||
$this->paidDates = [];
|
||||
|
||||
|
||||
// start with currencies:
|
||||
/** @var Bill $object */
|
||||
foreach ($objects as $object) {
|
||||
$id = (int)$object->transaction_currency_id;
|
||||
$bills[] = (int)$object->id;
|
||||
$currencies[$id] = $currencies[$id] ?? TransactionCurrency::find($id);
|
||||
}
|
||||
$this->currencies = $currencies;
|
||||
|
||||
// continue with notes
|
||||
$notes = Note::whereNoteableType(Bill::class)->whereIn('noteable_id', array_keys($bills))->get();
|
||||
/** @var Note $note */
|
||||
foreach ($notes as $note) {
|
||||
$id = (int)$note->noteable_id;
|
||||
$this->notes[$id] = $note;
|
||||
}
|
||||
// grab object groups:
|
||||
$set = DB::table('object_groupables')
|
||||
->leftJoin('object_groups', 'object_groups.id', '=', 'object_groupables.object_group_id')
|
||||
->where('object_groupables.object_groupable_type', Bill::class)
|
||||
->get(['object_groupables.*', 'object_groups.title', 'object_groups.order']);
|
||||
/** @var ObjectGroup $entry */
|
||||
foreach ($set as $entry) {
|
||||
$billId = (int)$entry->object_groupable_id;
|
||||
$id = (int)$entry->object_group_id;
|
||||
$order = (int)$entry->order;
|
||||
$this->groups[$billId] = [
|
||||
'object_group_id' => $id,
|
||||
'object_group_title' => $entry->title,
|
||||
'object_group_order' => $order,
|
||||
];
|
||||
|
||||
}
|
||||
$this->default = app('amount')->getDefaultCurrency();
|
||||
|
||||
// grab all paid dates:
|
||||
if (null !== $this->parameters->get('start') && null !== $this->parameters->get('end')) {
|
||||
$journals = TransactionJournal::whereIn('bill_id', $bills)
|
||||
->where('date', '>=', $this->parameters->get('start'))
|
||||
->where('date', '<=', $this->parameters->get('end'))
|
||||
->get(['transaction_journals.id', 'transaction_journals.transaction_group_id', 'transaction_journals.date', 'transaction_journals.bill_id']);
|
||||
/** @var TransactionJournal $journal */
|
||||
foreach ($journals as $journal) {
|
||||
$billId = (int)$journal->bill_id;
|
||||
$this->paidDates[$billId][] = [
|
||||
'transaction_group_id' => (string)$journal->id,
|
||||
'transaction_journal_id' => (string)$journal->transaction_group_id,
|
||||
'date' => $journal->date->toAtomString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the bill.
|
||||
*
|
||||
* @param Bill $bill
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function transform(Bill $bill): array
|
||||
{
|
||||
$paidData = $this->paidDates[(int)$bill->id] ?? [];
|
||||
$nextExpectedMatch = $this->nextExpectedMatch($bill, $this->paidDates[(int)$bill->id] ?? []);
|
||||
$payDates = $this->payDates($bill);
|
||||
$currency = $this->currencies[(int)$bill->transaction_currency_id];
|
||||
$group = $this->groups[(int)$bill->id] ?? null;
|
||||
$nextExpectedMatchDiff = $this->getNextExpectedMatchDiff($nextExpectedMatch, $payDates);
|
||||
return [
|
||||
'id' => (int)$bill->id,
|
||||
'created_at' => $bill->created_at->toAtomString(),
|
||||
'updated_at' => $bill->updated_at->toAtomString(),
|
||||
'name' => $bill->name,
|
||||
'amount_min' => app('steam')->bcround($bill->amount_min, $currency->decimal_places),
|
||||
'amount_max' => app('steam')->bcround($bill->amount_max, $currency->decimal_places),
|
||||
'currency_id' => (string)$bill->transaction_currency_id,
|
||||
'currency_code' => $currency->code,
|
||||
'currency_symbol' => $currency->symbol,
|
||||
'currency_decimal_places' => (int)$currency->decimal_places,
|
||||
'date' => $bill->date->toAtomString(),
|
||||
'end_date' => $bill->end_date?->toAtomString(),
|
||||
'extension_date' => $bill->extension_date?->toAtomString(),
|
||||
'repeat_freq' => $bill->repeat_freq,
|
||||
'skip' => (int)$bill->skip,
|
||||
'active' => $bill->active,
|
||||
'order' => (int)$bill->order,
|
||||
'notes' => $this->notes[(int)$bill->id] ?? null,
|
||||
'object_group_id' => $group ? $group['object_group_id'] : null,
|
||||
'object_group_order' => $group ? $group['object_group_order'] : null,
|
||||
'object_group_title' => $group ? $group['object_group_title'] : null,
|
||||
'next_expected_match' => $nextExpectedMatch->toAtomString(),
|
||||
'next_expected_match_diff' => $nextExpectedMatchDiff,
|
||||
'pay_dates' => $payDates,
|
||||
'paid_dates' => $paidData,
|
||||
'links' => [
|
||||
[
|
||||
'rel' => 'self',
|
||||
'uri' => sprintf('/bills/%d', $bill->id),
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data the bill was paid and predict the next expected match.
|
||||
*
|
||||
* @param Bill $bill
|
||||
* @param array $dates
|
||||
*
|
||||
* @return Carbon
|
||||
*/
|
||||
protected function nextExpectedMatch(Bill $bill, array $dates): Carbon
|
||||
{
|
||||
// 2023-07-1 sub one day from the start date to fix a possible bug (see #7704)
|
||||
// 2023-07-18 this particular date is used to search for the last paid date.
|
||||
// 2023-07-18 the cloned $searchDate is used to grab the correct transactions.
|
||||
/** @var Carbon $start */
|
||||
$start = clone $this->parameters->get('start');
|
||||
$start->subDay();
|
||||
|
||||
$lastPaidDate = $this->lastPaidDate($dates, $start);
|
||||
$nextMatch = clone $bill->date;
|
||||
while ($nextMatch < $lastPaidDate) {
|
||||
/*
|
||||
* As long as this date is smaller than the last time the bill was paid, keep jumping ahead.
|
||||
* For example: 1 jan, 1 feb, etc.
|
||||
*/
|
||||
$nextMatch = app('navigation')->addPeriod($nextMatch, $bill->repeat_freq, $bill->skip);
|
||||
}
|
||||
if ($nextMatch->isSameDay($lastPaidDate)) {
|
||||
/*
|
||||
* Add another period because it's the same day as the last paid date.
|
||||
*/
|
||||
$nextMatch = app('navigation')->addPeriod($nextMatch, $bill->repeat_freq, $bill->skip);
|
||||
}
|
||||
return $nextMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest date in the set, or start when set is empty.
|
||||
*
|
||||
* @param Collection $dates
|
||||
* @param Carbon $default
|
||||
*
|
||||
* @return Carbon
|
||||
*/
|
||||
protected function lastPaidDate(array $dates, Carbon $default): Carbon
|
||||
{
|
||||
if (0 === count($dates)) {
|
||||
return $default;
|
||||
}
|
||||
$latest = $dates[0]['date'];
|
||||
/** @var array $row */
|
||||
foreach ($dates as $row) {
|
||||
$carbon = new Carbon($row['date']);
|
||||
if ($carbon->gte($latest)) {
|
||||
$latest = $row['date'];
|
||||
}
|
||||
}
|
||||
|
||||
return new Carbon($latest);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Bill $bill
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function payDates(Bill $bill): array
|
||||
{
|
||||
//Log::debug(sprintf('Now in payDates() for bill #%d', $bill->id));
|
||||
if (null === $this->parameters->get('start') || null === $this->parameters->get('end')) {
|
||||
//Log::debug('No start or end date, give empty array.');
|
||||
|
||||
return [];
|
||||
}
|
||||
$set = new Collection();
|
||||
$currentStart = clone $this->parameters->get('start');
|
||||
// 2023-06-23 subDay to fix 7655
|
||||
$currentStart->subDay();
|
||||
$loop = 0;
|
||||
while ($currentStart <= $this->parameters->get('end')) {
|
||||
$nextExpectedMatch = $this->nextDateMatch($bill, $currentStart);
|
||||
// If nextExpectedMatch is after end, we continue:
|
||||
if ($nextExpectedMatch > $this->parameters->get('end')) {
|
||||
break;
|
||||
}
|
||||
// add to set
|
||||
$set->push(clone $nextExpectedMatch);
|
||||
$nextExpectedMatch->addDay();
|
||||
$currentStart = clone $nextExpectedMatch;
|
||||
$loop++;
|
||||
if ($loop > 4) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$simple = $set->map(
|
||||
static function (Carbon $date) {
|
||||
return $date->toAtomString();
|
||||
}
|
||||
);
|
||||
|
||||
return $simple->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a bill and a date, this method will tell you at which moment this bill expects its next
|
||||
* transaction. Whether or not it is there already, is not relevant.
|
||||
*
|
||||
* @param Bill $bill
|
||||
* @param Carbon $date
|
||||
*
|
||||
* @return Carbon
|
||||
*/
|
||||
protected function nextDateMatch(Bill $bill, Carbon $date): Carbon
|
||||
{
|
||||
//Log::debug(sprintf('Now in nextDateMatch(%d, %s)', $bill->id, $date->format('Y-m-d')));
|
||||
$start = clone $bill->date;
|
||||
//Log::debug(sprintf('Bill start date is %s', $start->format('Y-m-d')));
|
||||
while ($start < $date) {
|
||||
$start = app('navigation')->addPeriod($start, $bill->repeat_freq, $bill->skip);
|
||||
}
|
||||
|
||||
//Log::debug(sprintf('End of loop, bill start date is now %s', $start->format('Y-m-d')));
|
||||
|
||||
return $start;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Carbon $next
|
||||
* @param array $dates
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getNextExpectedMatchDiff(Carbon $next, array $dates): string
|
||||
{
|
||||
if ($next->isToday()) {
|
||||
return trans('firefly.today');
|
||||
}
|
||||
$current = $dates[0] ?? null;
|
||||
if (null === $current) {
|
||||
return trans('firefly.not_expected_period');
|
||||
}
|
||||
$carbon = new Carbon($current);
|
||||
return $carbon->diffForHumans(today(config('app.timezone')), CarbonInterface::DIFF_RELATIVE_TO_NOW);
|
||||
}
|
||||
|
||||
|
||||
}
|
269
app/Transformers/V2/PiggyBankTransformer.php
Normal file
269
app/Transformers/V2/PiggyBankTransformer.php
Normal file
@ -0,0 +1,269 @@
|
||||
<?php
|
||||
/**
|
||||
* PiggyBankTransformer.php
|
||||
* Copyright (c) 2019 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Transformers\V2;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\AccountMeta;
|
||||
use FireflyIII\Models\Note;
|
||||
use FireflyIII\Models\ObjectGroup;
|
||||
use FireflyIII\Models\PiggyBank;
|
||||
use FireflyIII\Models\PiggyBankRepetition;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use JsonException;
|
||||
|
||||
/**
|
||||
* Class PiggyBankTransformer
|
||||
*/
|
||||
class PiggyBankTransformer extends AbstractTransformer
|
||||
{
|
||||
// private AccountRepositoryInterface $accountRepos;
|
||||
// private CurrencyRepositoryInterface $currencyRepos;
|
||||
// private PiggyBankRepositoryInterface $piggyRepos;
|
||||
private array $accounts;
|
||||
private ExchangeRateConverter $converter;
|
||||
private array $currencies;
|
||||
private TransactionCurrency $default;
|
||||
private array $groups;
|
||||
private array $notes;
|
||||
private array $repetitions;
|
||||
|
||||
/**
|
||||
* PiggyBankTransformer constructor.
|
||||
*
|
||||
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->notes = [];
|
||||
$this->accounts = [];
|
||||
$this->groups = [];
|
||||
$this->currencies = [];
|
||||
$this->repetitions = [];
|
||||
// $this->
|
||||
// $this->currencyRepos = app(CurrencyRepositoryInterface::class);
|
||||
// $this->piggyRepos = app(PiggyBankRepositoryInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function collectMetaData(Collection $objects): void
|
||||
{
|
||||
// TODO move to repository (does not exist yet)
|
||||
$piggyBanks = $objects->pluck('id')->toArray();
|
||||
$accountInfo = Account::whereIn('id', $objects->pluck('account_id')->toArray())->get();
|
||||
$currencyPreferences = AccountMeta::where('name', '"currency_id"')->whereIn('account_id', $objects->pluck('account_id')->toArray())->get();
|
||||
/** @var Account $account */
|
||||
foreach ($accountInfo as $account) {
|
||||
$id = (int)$account->id;
|
||||
$this->accounts[$id] = [
|
||||
'name' => $account->name,
|
||||
];
|
||||
}
|
||||
/** @var AccountMeta $preference */
|
||||
foreach ($currencyPreferences as $preference) {
|
||||
$currencyId = (int)$preference->data;
|
||||
$accountId = (int)$preference->account_id;
|
||||
$currencies[$currencyId] = $currencies[$currencyId] ?? TransactionJournal::find($currencyId);
|
||||
$this->currencies[$accountId] = $currencies[$currencyId];
|
||||
}
|
||||
|
||||
// grab object groups:
|
||||
$set = DB::table('object_groupables')
|
||||
->leftJoin('object_groups', 'object_groups.id', '=', 'object_groupables.object_group_id')
|
||||
->where('object_groupables.object_groupable_type', PiggyBank::class)
|
||||
->get(['object_groupables.*', 'object_groups.title', 'object_groups.order']);
|
||||
/** @var ObjectGroup $entry */
|
||||
foreach ($set as $entry) {
|
||||
$piggyBankId = (int)$entry->object_groupable_id;
|
||||
$id = (int)$entry->object_group_id;
|
||||
$order = (int)$entry->order;
|
||||
$this->groups[$piggyBankId] = [
|
||||
'object_group_id' => $id,
|
||||
'object_group_title' => $entry->title,
|
||||
'object_group_order' => $order,
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
// grab repetitions (for current amount):
|
||||
$repetitions = PiggyBankRepetition::whereIn('piggy_bank_id', $piggyBanks)->get();
|
||||
/** @var PiggyBankRepetition $repetition */
|
||||
foreach ($repetitions as $repetition) {
|
||||
$this->repetitions[(int)$repetition->piggy_bank_id] = [
|
||||
'amount' => $repetition->currentamount,
|
||||
];
|
||||
}
|
||||
|
||||
// grab notes
|
||||
// continue with notes
|
||||
$notes = Note::whereNoteableType(PiggyBank::class)->whereIn('noteable_id', array_keys($piggyBanks))->get();
|
||||
/** @var Note $note */
|
||||
foreach ($notes as $note) {
|
||||
$id = (int)$note->noteable_id;
|
||||
$this->notes[$id] = $note;
|
||||
}
|
||||
|
||||
$this->default = app('amount')->getDefaultCurrencyByUser(auth()->user());
|
||||
$this->converter = new ExchangeRateConverter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the piggy bank.
|
||||
*
|
||||
* @param PiggyBank $piggyBank
|
||||
*
|
||||
* @return array
|
||||
* @throws FireflyException
|
||||
* @throws JsonException
|
||||
*/
|
||||
public function transform(PiggyBank $piggyBank): array
|
||||
{
|
||||
// $account = $piggyBank->account;
|
||||
// $this->accountRepos->setUser($account->user);
|
||||
// $this->currencyRepos->setUser($account->user);
|
||||
// $this->piggyRepos->setUser($account->user);
|
||||
|
||||
// get currency from account, or use default.
|
||||
// $currency = $this->accountRepos->getAccountCurrency($account) ?? app('amount')->getDefaultCurrencyByUser($account->user);
|
||||
|
||||
// note
|
||||
// $notes = $this->piggyRepos->getNoteText($piggyBank);
|
||||
// $notes = '' === $notes ? null : $notes;
|
||||
|
||||
// $objectGroupId = null;
|
||||
// $objectGroupOrder = null;
|
||||
// $objectGroupTitle = null;
|
||||
// /** @var ObjectGroup $objectGroup */
|
||||
// $objectGroup = $piggyBank->objectGroups->first();
|
||||
// if (null !== $objectGroup) {
|
||||
// $objectGroupId = (int)$objectGroup->id;
|
||||
// $objectGroupOrder = (int)$objectGroup->order;
|
||||
// $objectGroupTitle = $objectGroup->title;
|
||||
// }
|
||||
|
||||
// get currently saved amount:
|
||||
// $currentAmount = app('steam')->bcround($this->piggyRepos->getCurrentAmount($piggyBank), $currency->decimal_places);
|
||||
|
||||
$percentage = null;
|
||||
$leftToSave = null;
|
||||
$nativeLeftToSave = null;
|
||||
$savePerMonth = null;
|
||||
$nativeSavePerMonth = null;
|
||||
$startDate = $piggyBank->startdate?->format('Y-m-d');
|
||||
$targetDate = $piggyBank->targetdate?->format('Y-m-d');
|
||||
$accountId = (int)$piggyBank->account_id;
|
||||
$accountName = $this->accounts[$accountId]['name'] ?? null;
|
||||
$currency = $this->currencies[$accountId] ?? $this->default;
|
||||
$currentAmount = app('steam')->bcround($this->repetitions[(int)$piggyBank->id]['amount'] ?? '0', $currency->decimal_places);
|
||||
$nativeCurrentAmount = $this->converter->convert($this->default, $currency, today(), $currentAmount);
|
||||
$targetAmount = $piggyBank->targetamount;
|
||||
$nativeTargetAmount = $this->converter->convert($this->default, $currency, today(), $targetAmount);
|
||||
$note = $this->notes[(int)$piggyBank->id] ?? null;
|
||||
$group = $this->groups[(int)$piggyBank->id] ?? null;
|
||||
|
||||
if (0 !== bccomp($targetAmount, '0')) { // target amount is not 0.00
|
||||
$leftToSave = bcsub($targetAmount, $currentAmount);
|
||||
$nativeLeftToSave = $this->converter->convert($this->default, $currency, today(), $leftToSave);
|
||||
$percentage = (int)bcmul(bcdiv($currentAmount, $targetAmount), '100');
|
||||
$savePerMonth = $this->getSuggestedMonthlyAmount($currentAmount, $targetAmount, $piggyBank->startdate, $piggyBank->targetdate);
|
||||
$nativeSavePerMonth = $this->converter->convert($this->default, $currency, today(), $savePerMonth);
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (string)$piggyBank->id,
|
||||
'created_at' => $piggyBank->created_at->toAtomString(),
|
||||
'updated_at' => $piggyBank->updated_at->toAtomString(),
|
||||
'account_id' => (string)$piggyBank->account_id,
|
||||
'account_name' => $accountName,
|
||||
'name' => $piggyBank->name,
|
||||
'currency_id' => (string)$currency->id,
|
||||
'currency_code' => $currency->code,
|
||||
'currency_symbol' => $currency->symbol,
|
||||
'currency_decimal_places' => (int)$currency->decimal_places,
|
||||
'native_id' => (string)$this->default->id,
|
||||
'native_code' => $this->default->code,
|
||||
'native_symbol' => $this->default->symbol,
|
||||
'native_decimal_places' => (int)$this->default->decimal_places,
|
||||
'current_amount' => $currentAmount,
|
||||
'native_current_amount' => $nativeCurrentAmount,
|
||||
'target_amount' => $targetAmount,
|
||||
'native_target_amount' => $nativeTargetAmount,
|
||||
'percentage' => $percentage,
|
||||
'left_to_save' => $leftToSave,
|
||||
'native_left_to_save' => $nativeLeftToSave,
|
||||
'save_per_month' => $savePerMonth,
|
||||
'native_save_per_month' => $nativeSavePerMonth,
|
||||
'start_date' => $startDate,
|
||||
'target_date' => $targetDate,
|
||||
'order' => (int)$piggyBank->order,
|
||||
'active' => $piggyBank->active,
|
||||
'notes' => $note,
|
||||
'object_group_id' => $group ? $group['object_group_id'] : null,
|
||||
'object_group_order' => $group ? $group['object_group_order'] : null,
|
||||
'object_group_title' => $group ? $group['object_group_title'] : null,
|
||||
'links' => [
|
||||
[
|
||||
'rel' => 'self',
|
||||
'uri' => '/piggy_banks/' . $piggyBank->id,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
private function getSuggestedMonthlyAmount(string $currentAmount, string $targetAmount, ?Carbon $startDate, ?Carbon $targetDate): string
|
||||
{
|
||||
$savePerMonth = '0';
|
||||
if (null === $targetDate) {
|
||||
return '0';
|
||||
}
|
||||
if (bccomp($currentAmount, $targetAmount) < 1) {
|
||||
$now = today(config('app.timezone'));
|
||||
$startDate = null !== $startDate && $startDate->gte($now) ? $startDate : $now;
|
||||
$diffInMonths = $startDate->diffInMonths($targetDate, false);
|
||||
$remainingAmount = bcsub($targetAmount, $currentAmount);
|
||||
|
||||
// more than 1 month to go and still need money to save:
|
||||
if ($diffInMonths > 0 && 1 === bccomp($remainingAmount, '0')) {
|
||||
$savePerMonth = bcdiv($remainingAmount, (string)$diffInMonths);
|
||||
}
|
||||
|
||||
// less than 1 month to go but still need money to save:
|
||||
if (0 === $diffInMonths && 1 === bccomp($remainingAmount, '0')) {
|
||||
$savePerMonth = $remainingAmount;
|
||||
}
|
||||
}
|
||||
|
||||
return $savePerMonth;
|
||||
}
|
||||
}
|
@ -25,13 +25,19 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Transformers\V2;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Models\Note;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Models\TransactionJournal;
|
||||
use FireflyIII\Models\TransactionJournalMeta;
|
||||
use FireflyIII\Models\TransactionType;
|
||||
use FireflyIII\Support\Http\Api\ConvertsExchangeRates;
|
||||
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
|
||||
use FireflyIII\Support\NullArrayObject;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Class TransactionGroupTransformer
|
||||
@ -40,9 +46,12 @@ class TransactionGroupTransformer extends AbstractTransformer
|
||||
{
|
||||
use ConvertsExchangeRates;
|
||||
|
||||
private array $currencies = [];
|
||||
private TransactionCurrency $default;
|
||||
private array $meta;
|
||||
private ExchangeRateConverter $converter;
|
||||
private array $currencies = [];
|
||||
private TransactionCurrency $default;
|
||||
private array $meta;
|
||||
private array $notes;
|
||||
private array $tags;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
@ -55,15 +64,12 @@ class TransactionGroupTransformer extends AbstractTransformer
|
||||
/** @var array $object */
|
||||
foreach ($objects as $object) {
|
||||
foreach ($object['sums'] as $sum) {
|
||||
$id = $sum['currency_id'];
|
||||
if (!array_key_exists($id, $currencies)) {
|
||||
$currencyObject = TransactionCurrency::find($sum['currency_id']);
|
||||
$currencies[$id] = $currencyObject;
|
||||
}
|
||||
$id = (int)$sum['currency_id'];
|
||||
$currencies[$id] = $currencies[$id] ?? TransactionCurrency::find($sum['currency_id']);
|
||||
}
|
||||
/** @var array $transaction */
|
||||
foreach ($object['transactions'] as $transaction) {
|
||||
$id = $transaction['transaction_journal_id'];
|
||||
$id = (int)$transaction['transaction_journal_id'];
|
||||
$journals[$id] = [];
|
||||
}
|
||||
}
|
||||
@ -77,6 +83,28 @@ class TransactionGroupTransformer extends AbstractTransformer
|
||||
$id = (int)$entry->transaction_journal_id;
|
||||
$this->meta[$id][$entry->name] = $entry->data;
|
||||
}
|
||||
|
||||
// grab all notes for all journals:
|
||||
$notes = Note::whereNoteableType(TransactionJournal::class)->whereIn('noteable_id', array_keys($journals))->get();
|
||||
/** @var Note $note */
|
||||
foreach ($notes as $note) {
|
||||
$id = (int)$note->noteable_id;
|
||||
$this->notes[$id] = $note;
|
||||
}
|
||||
|
||||
// grab all tags for all journals:
|
||||
$tags = DB::table('tag_transaction_journal')
|
||||
->leftJoin('tags', 'tags.id', 'tag_transaction_journal.tag_id')
|
||||
->whereIn('tag_transaction_journal.transaction_journal_id', array_keys($journals))
|
||||
->get(['tag_transaction_journal.transaction_journal_id', 'tags.tag']);
|
||||
/** @var stdClass $tag */
|
||||
foreach ($tags as $tag) {
|
||||
$id = (int)$tag->transaction_journal_id;
|
||||
$this->tags[$id][] = $tag->tag;
|
||||
}
|
||||
|
||||
// create converter
|
||||
$this->converter = new ExchangeRateConverter();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -92,6 +120,7 @@ class TransactionGroupTransformer extends AbstractTransformer
|
||||
'created_at' => $first['created_at']->toAtomString(),
|
||||
'updated_at' => $first['updated_at']->toAtomString(),
|
||||
'user' => (string)$first['user_id'],
|
||||
'user_group' => (string)$first['user_group_id'],
|
||||
'group_title' => $group['title'] ?? null,
|
||||
'transactions' => $this->transformTransactions($group['transactions'] ?? []),
|
||||
'links' => [
|
||||
@ -122,108 +151,102 @@ class TransactionGroupTransformer extends AbstractTransformer
|
||||
* @param array $transaction
|
||||
*
|
||||
* @return array
|
||||
* @throws FireflyException
|
||||
*/
|
||||
private function transformTransaction(array $transaction): array
|
||||
{
|
||||
$transaction = new NullArrayObject($transaction);
|
||||
$journalId = (int)$transaction['transaction_journal_id'];
|
||||
$meta = new NullArrayObject($this->meta[$journalId] ?? []);
|
||||
$type = $this->stringFromArray($transaction, 'transaction_type_type', TransactionType::WITHDRAWAL);
|
||||
$transaction = new NullArrayObject($transaction);
|
||||
$type = $this->stringFromArray($transaction, 'transaction_type_type', TransactionType::WITHDRAWAL);
|
||||
$journalId = (int)$transaction['transaction_journal_id'];
|
||||
$meta = new NullArrayObject($this->meta[$journalId] ?? []);
|
||||
|
||||
/**
|
||||
* Convert and use amount:
|
||||
*/
|
||||
$amount = app('steam')->positive((string)($transaction['amount'] ?? '0'));
|
||||
$currencyId = (int)$transaction['currency_id'];
|
||||
$nativeAmount = $this->converter->convert($this->default, $this->currencies[$currencyId], $transaction['date'], $amount);
|
||||
$foreignAmount = null;
|
||||
$nativeForeignAmount = null;
|
||||
if (null !== $transaction['foreign_amount']) {
|
||||
$foreignCurrencyId = (int)$transaction['foreign_currency_id'];
|
||||
$foreignAmount = app('steam')->positive($transaction['foreign_amount']);
|
||||
$nativeForeignAmount = $foreignAmount;
|
||||
if ($transaction['foreign_currency_id'] !== $this->default->id) {
|
||||
$rate = $this->getRate($this->currencies[$transaction['foreign_currency_id']], $this->default, $transaction['date']);
|
||||
$nativeForeignAmount = bcmul($foreignAmount, $rate);
|
||||
}
|
||||
}
|
||||
|
||||
$nativeAmount = $amount;
|
||||
if ($transaction['currency_id'] !== $this->default->id) {
|
||||
$rate = $this->getRate($this->currencies[$transaction['currency_id']], $this->default, $transaction['date']);
|
||||
$nativeAmount = bcmul($amount, $rate);
|
||||
$nativeForeignAmount = $this->converter->convert($this->default, $this->currencies[$foreignCurrencyId], $transaction['date'], $foreignAmount);
|
||||
}
|
||||
|
||||
return [
|
||||
'user' => (string)$transaction['user_id'],
|
||||
'transaction_journal_id' => (string)$transaction['transaction_journal_id'],
|
||||
'type' => strtolower($type),
|
||||
'date' => $transaction['date']->toAtomString(),
|
||||
'order' => $transaction['order'],
|
||||
'currency_id' => (string)$transaction['currency_id'],
|
||||
'currency_code' => $transaction['currency_code'],
|
||||
'currency_name' => $transaction['currency_name'],
|
||||
'currency_symbol' => $transaction['currency_symbol'],
|
||||
'currency_decimal_places' => (int)$transaction['currency_decimal_places'],
|
||||
'user' => (string)$transaction['user_id'],
|
||||
'user_group' => (string)$transaction['user_group_id'],
|
||||
'transaction_journal_id' => (string)$transaction['transaction_journal_id'],
|
||||
'type' => strtolower($type),
|
||||
'date' => $transaction['date']->toAtomString(),
|
||||
'order' => $transaction['order'],
|
||||
'amount' => $amount,
|
||||
'native_amount' => $nativeAmount,
|
||||
'foreign_amount' => $foreignAmount,
|
||||
'native_foreign_amount' => $nativeForeignAmount,
|
||||
'currency_id' => (string)$transaction['currency_id'],
|
||||
'currency_code' => $transaction['currency_code'],
|
||||
'currency_name' => $transaction['currency_name'],
|
||||
'currency_symbol' => $transaction['currency_symbol'],
|
||||
'currency_decimal_places' => (int)$transaction['currency_decimal_places'],
|
||||
|
||||
// converted to native currency
|
||||
'native_currency_converted' => $transaction['currency_id'] !== $this->default->id,
|
||||
'native_currency_id' => (string)$this->default->id,
|
||||
'native_currency_code' => $this->default->code,
|
||||
'native_currency_name' => $this->default->name,
|
||||
'native_currency_symbol' => $this->default->symbol,
|
||||
'native_currency_decimal_places' => (int)$this->default->decimal_places,
|
||||
'native_id' => (string)$this->default->id,
|
||||
'native_code' => $this->default->code,
|
||||
'native_name' => $this->default->name,
|
||||
'native_symbol' => $this->default->symbol,
|
||||
'native_decimal_places' => (int)$this->default->decimal_places,
|
||||
|
||||
// foreign currency amount:
|
||||
'foreign_currency_id' => $this->stringFromArray($transaction, 'foreign_currency_id', null),
|
||||
'foreign_currency_code' => $transaction['foreign_currency_code'],
|
||||
'foreign_currency_name' => $transaction['foreign_currency_name'],
|
||||
'foreign_currency_symbol' => $transaction['foreign_currency_symbol'],
|
||||
'foreign_currency_decimal_places' => $transaction['foreign_currency_decimal_places'],
|
||||
|
||||
// foreign converted to native currency:
|
||||
'foreign_currency_converted' => null !== $transaction['foreign_currency_id'] && $transaction['foreign_currency_id'] !== $this->default->id,
|
||||
|
||||
'amount' => $amount,
|
||||
'native_amount' => $nativeAmount,
|
||||
'foreign_amount' => $foreignAmount,
|
||||
'native_foreign_amount' => $nativeForeignAmount,
|
||||
'description' => $transaction['description'],
|
||||
'source_id' => (string)$transaction['source_account_id'],
|
||||
'source_name' => $transaction['source_account_name'],
|
||||
'source_iban' => $transaction['source_account_iban'],
|
||||
'source_type' => $transaction['source_account_type'],
|
||||
'destination_id' => (string)$transaction['destination_account_id'],
|
||||
'destination_name' => $transaction['destination_account_name'],
|
||||
'destination_iban' => $transaction['destination_account_iban'],
|
||||
'destination_type' => $transaction['destination_account_type'],
|
||||
'budget_id' => $this->stringFromArray($transaction, 'budget_id', null),
|
||||
'budget_name' => $transaction['budget_name'],
|
||||
'category_id' => $this->stringFromArray($transaction, 'category_id', null),
|
||||
'category_name' => $transaction['category_name'],
|
||||
'bill_id' => $this->stringFromArray($transaction, 'bill_id', null),
|
||||
'bill_name' => $transaction['bill_name'],
|
||||
'reconciled' => $transaction['reconciled'],
|
||||
|
||||
//'notes' => $this->groupRepos->getNoteText((int) $row['transaction_journal_id']),
|
||||
//'tags' => $this->groupRepos->getTags((int) $row['transaction_journal_id']),
|
||||
|
||||
'internal_reference' => $meta['internal_reference'],
|
||||
'external_id' => $meta['external_id'],
|
||||
'original_source' => $meta['original_source'],
|
||||
'recurrence_id' => $meta['recurrence_id'],
|
||||
'recurrence_total' => $meta['recurrence_total'],
|
||||
'recurrence_count' => $meta['recurrence_count'],
|
||||
'bunq_payment_id' => $meta['bunq_payment_id'],
|
||||
'external_url' => $meta['external_url'],
|
||||
'import_hash_v2' => $meta['import_hash_v2'],
|
||||
'sepa_cc' => $meta['sepa_cc'],
|
||||
'sepa_ct_op' => $meta['sepa_ct_op'],
|
||||
'sepa_ct_id' => $meta['sepa_ct_id'],
|
||||
'sepa_db' => $meta['sepa_db'],
|
||||
'sepa_country' => $meta['sepa_country'],
|
||||
'sepa_ep' => $meta['sepa_ep'],
|
||||
'sepa_ci' => $meta['sepa_ci'],
|
||||
'sepa_batch_id' => $meta['sepa_batch_id'],
|
||||
|
||||
'interest_date' => $this->date($meta['interest_date']),
|
||||
'book_date' => $this->date($meta['book_date']),
|
||||
'process_date' => $this->date($meta['process_date']),
|
||||
'due_date' => $this->date($meta['due_date']),
|
||||
'payment_date' => $this->date($meta['payment_date']),
|
||||
'invoice_date' => $this->date($meta['invoice_date']),
|
||||
// foreign converted to native:
|
||||
'description' => $transaction['description'],
|
||||
'source_id' => (string)$transaction['source_account_id'],
|
||||
'source_name' => $transaction['source_account_name'],
|
||||
'source_iban' => $transaction['source_account_iban'],
|
||||
'source_type' => $transaction['source_account_type'],
|
||||
'destination_id' => (string)$transaction['destination_account_id'],
|
||||
'destination_name' => $transaction['destination_account_name'],
|
||||
'destination_iban' => $transaction['destination_account_iban'],
|
||||
'destination_type' => $transaction['destination_account_type'],
|
||||
'budget_id' => $this->stringFromArray($transaction, 'budget_id', null),
|
||||
'budget_name' => $transaction['budget_name'],
|
||||
'category_id' => $this->stringFromArray($transaction, 'category_id', null),
|
||||
'category_name' => $transaction['category_name'],
|
||||
'bill_id' => $this->stringFromArray($transaction, 'bill_id', null),
|
||||
'bill_name' => $transaction['bill_name'],
|
||||
'reconciled' => $transaction['reconciled'],
|
||||
'notes' => $this->notes[$journalId] ?? null,
|
||||
'tags' => $this->tags[$journalId] ?? [],
|
||||
'internal_reference' => $meta['internal_reference'],
|
||||
'external_id' => $meta['external_id'],
|
||||
'original_source' => $meta['original_source'],
|
||||
'recurrence_id' => $meta['recurrence_id'],
|
||||
'recurrence_total' => $meta['recurrence_total'],
|
||||
'recurrence_count' => $meta['recurrence_count'],
|
||||
'bunq_payment_id' => $meta['bunq_payment_id'],
|
||||
'external_url' => $meta['external_url'],
|
||||
'import_hash_v2' => $meta['import_hash_v2'],
|
||||
'sepa_cc' => $meta['sepa_cc'],
|
||||
'sepa_ct_op' => $meta['sepa_ct_op'],
|
||||
'sepa_ct_id' => $meta['sepa_ct_id'],
|
||||
'sepa_db' => $meta['sepa_db'],
|
||||
'sepa_country' => $meta['sepa_country'],
|
||||
'sepa_ep' => $meta['sepa_ep'],
|
||||
'sepa_ci' => $meta['sepa_ci'],
|
||||
'sepa_batch_id' => $meta['sepa_batch_id'],
|
||||
'interest_date' => $this->date($meta['interest_date']),
|
||||
'book_date' => $this->date($meta['book_date']),
|
||||
'process_date' => $this->date($meta['process_date']),
|
||||
'due_date' => $this->date($meta['due_date']),
|
||||
'payment_date' => $this->date($meta['payment_date']),
|
||||
'invoice_date' => $this->date($meta['invoice_date']),
|
||||
|
||||
// location data
|
||||
// 'longitude' => $longitude,
|
||||
@ -237,6 +260,9 @@ class TransactionGroupTransformer extends AbstractTransformer
|
||||
/**
|
||||
* TODO also in the old transformer.
|
||||
*
|
||||
* Used to extract a value from the given array, and fall back on a sensible default or NULL
|
||||
* if it can't be helped.
|
||||
*
|
||||
* @param NullArrayObject $array
|
||||
* @param string $key
|
||||
* @param string|null $default
|
||||
@ -280,6 +306,13 @@ class TransactionGroupTransformer extends AbstractTransformer
|
||||
if (10 === strlen($string)) {
|
||||
return Carbon::createFromFormat('Y-m-d', $string, config('app.timezone'));
|
||||
}
|
||||
if (25 === strlen($string)) {
|
||||
return Carbon::parse($string, config('app.timezone'));
|
||||
}
|
||||
if (19 === strlen($string) && str_contains($string, 'T')) {
|
||||
return Carbon::createFromFormat('Y-m-d\TH:i:s', substr($string, 0, 19), config('app.timezone'));
|
||||
}
|
||||
|
||||
// 2022-01-01 01:01:01
|
||||
return Carbon::createFromFormat('Y-m-d H:i:s', substr($string, 0, 19), config('app.timezone'));
|
||||
}
|
||||
|
2
artisan
2
artisan
@ -11,7 +11,7 @@ define('LARAVEL_START', microtime(true));
|
||||
| Composer provides a convenient, automatically generated class loader
|
||||
| for our application. We just need to utilize it! We'll require it
|
||||
| into the script here so that we do not have to worry about the
|
||||
| loading of any our classes "manually". Feels great to relax.
|
||||
| loading of any of our classes manually. It's great to relax.
|
||||
|
|
||||
*/
|
||||
|
||||
|
17
changelog.md
17
changelog.md
@ -3,6 +3,23 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## 6.0.20 - 2023-08-13
|
||||
|
||||
### Fixed
|
||||
|
||||
- [Issue 7787](https://github.com/firefly-iii/firefly-iii/issues/7787) Possible issue when deleting multiple tags from a transaction.
|
||||
- [Issue 7792](https://github.com/firefly-iii/firefly-iii/issues/7792) Search for tags was broken in rules
|
||||
- [Issue 7803](https://github.com/firefly-iii/firefly-iii/issues/7803) @zqye fixed an issue where the cron job would fire when not necessary.
|
||||
- [Issue 7771](https://github.com/firefly-iii/firefly-iii/issues/7771) Unclear use of language in rule trigger
|
||||
- [Issue 7818](https://github.com/firefly-iii/firefly-iii/issues/7818) Amount was negative instead of positive.
|
||||
- [Issue 7810](https://github.com/firefly-iii/firefly-iii/issues/7810) Bad math
|
||||
- Asset accounts will correctly show transaction groups
|
||||
|
||||
### API
|
||||
|
||||
- Lots of new, undocumented v2 API endpoints.
|
||||
- [Issue 7845](https://github.com/firefly-iii/firefly-iii/issues/7845) Could not reconcile over API
|
||||
|
||||
## 6.0.19 - 2023-07-29
|
||||
|
||||
### Fixed
|
||||
|
@ -94,7 +94,7 @@
|
||||
"laravel/slack-notification-channel": "^3",
|
||||
"laravel/ui": "^4.2",
|
||||
"league/commonmark": "2.*",
|
||||
"league/csv": "^9.7",
|
||||
"league/csv": "^9.10",
|
||||
"league/fractal": "0.*",
|
||||
"nunomaduro/collision": "^7.7",
|
||||
"pragmarx/google2fa": "^8.0",
|
||||
@ -105,8 +105,8 @@
|
||||
"spatie/laravel-html": "^3.2",
|
||||
"spatie/laravel-ignition": "^2",
|
||||
"spatie/period": "^2.4",
|
||||
"symfony/http-client": "^6.2",
|
||||
"symfony/mailgun-mailer": "^6.2",
|
||||
"symfony/http-client": "^6.3",
|
||||
"symfony/mailgun-mailer": "^6.3",
|
||||
"therobfonz/laravel-mandrill-driver": "^5.0"
|
||||
},
|
||||
"require-dev": {
|
||||
|
501
composer.lock
generated
501
composer.lock
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user