chore: reformat code.

This commit is contained in:
James Cole
2023-06-21 12:34:58 +02:00
parent 8d87abde64
commit 3dcb35710b
799 changed files with 23319 additions and 22173 deletions

View File

@@ -30,12 +30,9 @@ SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
# clean up php code # clean up php code
cd $SCRIPT_DIR/php-cs-fixer cd $SCRIPT_DIR/php-cs-fixer
composer update composer update --quiet
rm -f .php-cs-fixer.cache rm -f .php-cs-fixer.cache
echo 'Removed cache...'
echo 'Running...'
PHP_CS_FIXER_IGNORE_ENV=true ./vendor/bin/php-cs-fixer fix --config $SCRIPT_DIR/php-cs-fixer/.php-cs-fixer.php --allow-risky=yes PHP_CS_FIXER_IGNORE_ENV=true ./vendor/bin/php-cs-fixer fix --config $SCRIPT_DIR/php-cs-fixer/.php-cs-fixer.php --allow-risky=yes
echo 'Done!'
cd $SCRIPT_DIR/.. cd $SCRIPT_DIR/..
exit 0 exit 0

37
.ci/phpmd.sh Normal file
View File

@@ -0,0 +1,37 @@
#!/usr/bin/env bash
#
# phpmd.sh
# 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/>.
#
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
cd $SCRIPT_DIR/phpmd
composer update --quiet
./vendor/bin/phpmd \
$SCRIPT_DIR/../app text phpmd.xml \
--exclude $SCRIPT_DIR/../app/resources/** \
--exclude $SCRIPT_DIR/../app/frontend/** \
--exclude $SCRIPT_DIR/../app/public/** \
--exclude $SCRIPT_DIR/../app/vendor/** \
cd $SCRIPT_DIR/..
exit 0

1
.ci/phpmd/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
vendor

5
.ci/phpmd/composer.json Normal file
View File

@@ -0,0 +1,5 @@
{
"require-dev": {
"phpmd/phpmd": "^2.13"
}
}

1012
.ci/phpmd/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

52
.ci/phpmd/phpmd.xml Normal file
View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="pcsg-generated-ruleset"
xmlns="http://pmd.sf.net/ruleset/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd"
xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd">
<description>Bla bla</description>
<!--
Commando vanuit firefly directory:
phpmd database,app,tests html /gdrive-all/development/phpmd/phpmd.xml > public/report.html
-->
<!-- Import the entire controversial code rule set -->
<rule ref="rulesets/controversial.xml">
<exclude name="CamelCasePropertyName" />
</rule>
<!-- clean code -->
<rule ref="rulesets/codesize.xml" />
<rule ref="rulesets/design.xml" />
<rule ref="rulesets/naming.xml" />
<rule ref="rulesets/unusedcode.xml" />
<rule ref="rulesets/codesize.xml/CyclomaticComplexity">
<properties>
<property name="reportLevel" value="5"/>
</properties>
</rule>
<rule ref="rulesets/codesize.xml/NPathComplexity">
<properties>
<property name="minimum" value="128"/>
</properties>
</rule>
<rule ref="rulesets/codesize.xml/ExcessiveMethodLength">
<properties>
<property name="minimum" value="40"/>
</properties>
</rule>
<rule ref="rulesets/codesize.xml/ExcessiveParameterList">
<properties>
<property name="minimum" value="5"/>
</properties>
</rule>
<!-- include clean code manually -->
<rule ref="rulesets/cleancode.xml/BooleanArgumentFlag" />
<rule ref="rulesets/cleancode.xml/ElseExpression" />
<!-- no this one -->
<!--<rule ref="rulesets/cleancode.xml/StaticAccess" />-->
</ruleset>

View File

@@ -143,6 +143,7 @@ MAIL_FROM=changeme@example.com
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=null MAIL_ENCRYPTION=null
MAIL_SENDMAIL_COMMAND=
# Other mail drivers: # Other mail drivers:
# If you use Docker or similar, you can set these variables from a file by appending them with _FILE # If you use Docker or similar, you can set these variables from a file by appending them with _FILE

View File

@@ -2,7 +2,10 @@
## Our Pledge ## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making
participation in our project and our community a harassment-free experience for everyone, regardless of age, body size,
disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race,
religion, or sexual identity and orientation.
## Our Standards ## Our Standards
@@ -24,23 +27,35 @@ Examples of unacceptable behavior by participants include:
## Our Responsibilities ## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take
appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits,
issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any
contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope ## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the
project or its community. Examples of representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed representative at an online or offline
event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement ## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at james@firefly-iii.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at
james@firefly-iii.org. The project team will review and investigate all complaints, and will respond in a way that it
deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the
reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent
repercussions as determined by other members of the project's leadership.
## Attribution ## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available
at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org [homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/ [version]: http://contributor-covenant.org/version/1/4/

View File

@@ -2,9 +2,11 @@
Sometimes bugs reported to Firefly III are configuration and system problems on the user's side. Sometimes bugs reported to Firefly III are configuration and system problems on the user's side.
If you run into any of the following problems, there's a good chance it's not a Firefly III issue, but a configuration issue. If you run into any of the following problems, there's a good chance it's not a Firefly III issue, but a configuration
issue.
- ⚠️ Firefly III can't connect to the database when starting or the password is wrong, even though you're sure it's correct. - ⚠️ Firefly III can't connect to the database when starting or the password is wrong, even though you're sure it's
correct.
- ⚠️ Errors about a missing `APP_KEY` or other encryption/hash problems - ⚠️ Errors about a missing `APP_KEY` or other encryption/hash problems
- ⚠️ You can't login due to `419` errors (page expired) - ⚠️ You can't login due to `419` errors (page expired)
- ⚠️ Any `500` error when starting Firefly III - ⚠️ Any `500` error when starting Firefly III
@@ -13,4 +15,6 @@ If you run into any of the following problems, there's a good chance it's not a
- ⚠️ Firefly III does not work behind your reverse proxy - ⚠️ Firefly III does not work behind your reverse proxy
- ⚠️ You can't connect to the Data Importer due to 404's or authentication issues. - ⚠️ You can't connect to the Data Importer due to 404's or authentication issues.
If you run into an issue like this, please start a [discussion](https://github.com/firefly-iii/firefly-iii/discussions) or chat on [Gitter.im](https://gitter.im/firefly-iii/firefly-iii). There's a good chance it's not a bug but something we can fix rather quickly :+1: If you run into an issue like this, please start a [discussion](https://github.com/firefly-iii/firefly-iii/discussions)
or chat on [Gitter.im](https://gitter.im/firefly-iii/firefly-iii). There's a good chance it's not a bug but something we
can fix rather quickly :+1:

6
.github/support.md vendored
View File

@@ -9,7 +9,8 @@ First of all: thank you for reporting a bug instead of ditching the tool altoget
1. Open bugs will have open issues, so search for one first. 1. Open bugs will have open issues, so search for one first.
2. If your feature request is already there, vote on it with :+1: or :-1: reactions. 2. If your feature request is already there, vote on it with :+1: or :-1: reactions.
3. Do NOT hijack old issues with the bug you found, open your own issue. 3. Do NOT hijack old issues with the bug you found, open your own issue.
4. If relevant, take the time and see if the [demo site](https://demo.firefly-iii.org/) is also suffering from your issue. 4. If relevant, take the time and see if the [demo site](https://demo.firefly-iii.org/) is also suffering from your
issue.
5. If relevant, read the [documentation](https://docs.firefly-iii.org/). 5. If relevant, read the [documentation](https://docs.firefly-iii.org/).
Please follow these guidelines when opening new issues: Please follow these guidelines when opening new issues:
@@ -25,7 +26,8 @@ Only then [create a new issue](https://github.com/firefly-iii/firefly-iii/issues
## Issue closure and abandonment policy ## Issue closure and abandonment policy
- Issues can be converted into discussions if it's not a bug or feature request. - Issues can be converted into discussions if it's not a bug or feature request.
- Features that won't be implemented will be labelled "wontfix". [This isn't personal](https://docs.firefly-iii.org/firefly-iii/about-firefly-iii/what-its-not/). - Features that won't be implemented will be labelled "
wontfix". [This isn't personal](https://docs.firefly-iii.org/firefly-iii/about-firefly-iii/what-its-not/).
- Issues can be closed if they're duplicates of other issues. - Issues can be closed if they're duplicates of other issues.
- Issues can be closed if the answer is in the FAQ. - Issues can be closed if the answer is in the FAQ.
- Issues will be closed automatically after 14 days. - Issues will be closed automatically after 14 days.

View File

@@ -7,8 +7,7 @@ jobs:
auto_comment: auto_comment:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - uses: aws-actions/closed-issue-message@v1
uses: aws-actions/closed-issue-message@v1
with: with:
message: | message: |
Hi there! This is an automatic reply. `Share and enjoy` Hi there! This is an automatic reply. `Share and enjoy`

View File

@@ -74,42 +74,6 @@ abstract class Controller extends BaseController
); );
} }
/**
* Method to help build URL's.
*
* @return string
*/
final protected function buildParams(): string
{
$return = '?';
$params = [];
foreach ($this->parameters as $key => $value) {
if ('page' === $key) {
continue;
}
if ($value instanceof Carbon) {
$params[$key] = $value->format('Y-m-d');
continue;
}
$params[$key] = $value;
}
return $return.http_build_query($params);
}
/**
* @return Manager
*/
final protected function getManager(): Manager
{
// create some objects:
$manager = new Manager();
$baseUrl = request()->getSchemeAndHttpHost().'/api/v1';
$manager->setSerializer(new JsonApiSerializer($baseUrl));
return $manager;
}
/** /**
* Method to grab all parameters from the URL. * Method to grab all parameters from the URL.
* *
@@ -211,4 +175,40 @@ abstract class Controller extends BaseController
return $bag; return $bag;
} }
/**
* Method to help build URL's.
*
* @return string
*/
final protected function buildParams(): string
{
$return = '?';
$params = [];
foreach ($this->parameters as $key => $value) {
if ('page' === $key) {
continue;
}
if ($value instanceof Carbon) {
$params[$key] = $value->format('Y-m-d');
continue;
}
$params[$key] = $value;
}
return $return . http_build_query($params);
}
/**
* @return Manager
*/
final protected function getManager(): Manager
{
// create some objects:
$manager = new Manager();
$baseUrl = request()->getSchemeAndHttpHost() . '/api/v1';
$manager->setSerializer(new JsonApiSerializer($baseUrl));
return $manager;
}
} }

View File

@@ -201,6 +201,91 @@ class DestroyController extends Controller
return response()->json([], 204); return response()->json([], 204);
} }
/**
*
*/
private function destroyBudgets(): void
{
/** @var AvailableBudgetRepositoryInterface $abRepository */
$abRepository = app(AvailableBudgetRepositoryInterface::class);
$abRepository->destroyAll();
/** @var BudgetLimitRepositoryInterface $blRepository */
$blRepository = app(BudgetLimitRepositoryInterface::class);
$blRepository->destroyAll();
/** @var BudgetRepositoryInterface $budgetRepository */
$budgetRepository = app(BudgetRepositoryInterface::class);
$budgetRepository->destroyAll();
}
/**
*
*/
private function destroyBills(): void
{
/** @var BillRepositoryInterface $repository */
$repository = app(BillRepositoryInterface::class);
$repository->destroyAll();
}
/**
*
*/
private function destroyPiggyBanks(): void
{
/** @var PiggyBankRepositoryInterface $repository */
$repository = app(PiggyBankRepositoryInterface::class);
$repository->destroyAll();
}
/**
*
*/
private function destroyRules(): void
{
/** @var RuleGroupRepositoryInterface $repository */
$repository = app(RuleGroupRepositoryInterface::class);
$repository->destroyAll();
}
/**
*
*/
private function destroyRecurringTransactions(): void
{
/** @var RecurringRepositoryInterface $repository */
$repository = app(RecurringRepositoryInterface::class);
$repository->destroyAll();
}
/**
*
*/
private function destroyCategories(): void
{
/** @var CategoryRepositoryInterface $categoryRepos */
$categoryRepos = app(CategoryRepositoryInterface::class);
$categoryRepos->destroyAll();
}
/**
*
*/
private function destroyTags(): void
{
/** @var TagRepositoryInterface $tagRepository */
$tagRepository = app(TagRepositoryInterface::class);
$tagRepository->destroyAll();
}
private function destroyObjectGroups(): void
{
/** @var ObjectGroupRepositoryInterface $repository */
$repository = app(ObjectGroupRepositoryInterface::class);
$repository->deleteAll();
}
/** /**
* @param array $types * @param array $types
*/ */
@@ -226,91 +311,6 @@ class DestroyController extends Controller
} }
} }
/**
*
*/
private function destroyBills(): void
{
/** @var BillRepositoryInterface $repository */
$repository = app(BillRepositoryInterface::class);
$repository->destroyAll();
}
/**
*
*/
private function destroyBudgets(): void
{
/** @var AvailableBudgetRepositoryInterface $abRepository */
$abRepository = app(AvailableBudgetRepositoryInterface::class);
$abRepository->destroyAll();
/** @var BudgetLimitRepositoryInterface $blRepository */
$blRepository = app(BudgetLimitRepositoryInterface::class);
$blRepository->destroyAll();
/** @var BudgetRepositoryInterface $budgetRepository */
$budgetRepository = app(BudgetRepositoryInterface::class);
$budgetRepository->destroyAll();
}
/**
*
*/
private function destroyCategories(): void
{
/** @var CategoryRepositoryInterface $categoryRepos */
$categoryRepos = app(CategoryRepositoryInterface::class);
$categoryRepos->destroyAll();
}
private function destroyObjectGroups(): void
{
/** @var ObjectGroupRepositoryInterface $repository */
$repository = app(ObjectGroupRepositoryInterface::class);
$repository->deleteAll();
}
/**
*
*/
private function destroyPiggyBanks(): void
{
/** @var PiggyBankRepositoryInterface $repository */
$repository = app(PiggyBankRepositoryInterface::class);
$repository->destroyAll();
}
/**
*
*/
private function destroyRecurringTransactions(): void
{
/** @var RecurringRepositoryInterface $repository */
$repository = app(RecurringRepositoryInterface::class);
$repository->destroyAll();
}
/**
*
*/
private function destroyRules(): void
{
/** @var RuleGroupRepositoryInterface $repository */
$repository = app(RuleGroupRepositoryInterface::class);
$repository->destroyAll();
}
/**
*
*/
private function destroyTags(): void
{
/** @var TagRepositoryInterface $tagRepository */
$tagRepository = app(TagRepositoryInterface::class);
$tagRepository->destroyAll();
}
/** /**
* @param array $types * @param array $types
*/ */

View File

@@ -69,6 +69,34 @@ class ExportController extends Controller
return $this->returnExport('accounts'); return $this->returnExport('accounts');
} }
/**
* @param string $key
*
* @return LaravelResponse
* @throws FireflyException
*/
private function returnExport(string $key): LaravelResponse
{
$date = date('Y-m-d-H-i-s');
$fileName = sprintf('%s-export-%s.csv', $date, $key);
$data = $this->exporter->export();
/** @var LaravelResponse $response */
$response = response($data[$key]);
$response
->header('Content-Description', 'File Transfer')
->header('Content-Type', 'application/octet-stream')
->header('Content-Disposition', 'attachment; filename=' . $fileName)
->header('Content-Transfer-Encoding', 'binary')
->header('Connection', 'Keep-Alive')
->header('Expires', '0')
->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
->header('Pragma', 'public')
->header('Content-Length', (string)strlen($data[$key]));
return $response;
}
/** /**
* This endpoint is documented at: * This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/data/exportBills * https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/data/exportBills
@@ -200,32 +228,4 @@ class ExportController extends Controller
return $this->returnExport('transactions'); return $this->returnExport('transactions');
} }
/**
* @param string $key
*
* @return LaravelResponse
* @throws FireflyException
*/
private function returnExport(string $key): LaravelResponse
{
$date = date('Y-m-d-H-i-s');
$fileName = sprintf('%s-export-%s.csv', $date, $key);
$data = $this->exporter->export();
/** @var LaravelResponse $response */
$response = response($data[$key]);
$response
->header('Content-Description', 'File Transfer')
->header('Content-Type', 'application/octet-stream')
->header('Content-Disposition', 'attachment; filename='.$fileName)
->header('Content-Transfer-Encoding', 'binary')
->header('Connection', 'Keep-Alive')
->header('Expires', '0')
->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
->header('Pragma', 'public')
->header('Content-Length', (string)strlen($data[$key]));
return $response;
}
} }

View File

@@ -97,6 +97,21 @@ class ShowController extends Controller
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
} }
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/transactions/getTransactionByJournal
*
* Show a single transaction, by transaction journal.
*
* @param TransactionJournal $transactionJournal
*
* @return JsonResponse
*/
public function showJournal(TransactionJournal $transactionJournal): JsonResponse
{
return $this->show($transactionJournal->transactionGroup);
}
/** /**
* This endpoint is documented at: * This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/transactions/getTransaction * https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/transactions/getTransaction
@@ -133,19 +148,4 @@ class ShowController extends Controller
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
} }
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/transactions/getTransactionByJournal
*
* Show a single transaction, by transaction journal.
*
* @param TransactionJournal $transactionJournal
*
* @return JsonResponse
*/
public function showJournal(TransactionJournal $transactionJournal): JsonResponse
{
return $this->show($transactionJournal->transactionGroup);
}
} }

View File

@@ -97,35 +97,6 @@ class UpdateController extends Controller
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
} }
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/currencies/enableCurrency
*
* Enable a currency.
*
* @param TransactionCurrency $currency
*
* @return JsonResponse
* @throws FireflyException
* @throws JsonException
*/
public function enable(TransactionCurrency $currency): JsonResponse
{
$this->repository->enable($currency);
$manager = $this->getManager();
$defaultCurrency = app('amount')->getDefaultCurrencyByUser(auth()->user());
$this->parameters->set('defaultCurrency', $defaultCurrency);
/** @var CurrencyTransformer $transformer */
$transformer = app(CurrencyTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new Item($currency, $transformer, 'currencies');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}
/** /**
* This endpoint is documented at: * This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/currencies/defaultCurrency * https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/currencies/defaultCurrency
@@ -157,6 +128,35 @@ class UpdateController extends Controller
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
} }
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/currencies/enableCurrency
*
* Enable a currency.
*
* @param TransactionCurrency $currency
*
* @return JsonResponse
* @throws FireflyException
* @throws JsonException
*/
public function enable(TransactionCurrency $currency): JsonResponse
{
$this->repository->enable($currency);
$manager = $this->getManager();
$defaultCurrency = app('amount')->getDefaultCurrencyByUser(auth()->user());
$this->parameters->set('defaultCurrency', $defaultCurrency);
/** @var CurrencyTransformer $transformer */
$transformer = app(CurrencyTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new Item($currency, $transformer, 'currencies');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}
/** /**
* This endpoint is documented at: * This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/currencies/updateCurrency * https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/currencies/updateCurrency

View File

@@ -33,7 +33,6 @@ use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use JsonException;
use League\Fractal\Pagination\IlluminatePaginatorAdapter; use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use League\Fractal\Resource\Collection as FractalCollection; use League\Fractal\Resource\Collection as FractalCollection;

View File

@@ -121,30 +121,6 @@ class BasicController extends Controller
return response()->json($return); return response()->json($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;
}
/** /**
* @param Carbon $start * @param Carbon $start
* @param Carbon $end * @param Carbon $end
@@ -422,4 +398,28 @@ class BasicController extends Controller
return $return; 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;
}
} }

View File

@@ -94,6 +94,42 @@ class ConfigurationController extends Controller
return response()->json($return); return response()->json($return);
} }
/**
* Get all config values.
*
* @return array
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getDynamicConfiguration(): array
{
$isDemoSite = app('fireflyconfig')->get('is_demo_site');
$updateCheck = app('fireflyconfig')->get('permission_update_check');
$lastCheck = app('fireflyconfig')->get('last_update_check');
$singleUser = app('fireflyconfig')->get('single_user_mode');
return [
'is_demo_site' => $isDemoSite?->data,
'permission_update_check' => null === $updateCheck ? null : (int)$updateCheck->data,
'last_update_check' => null === $lastCheck ? null : (int)$lastCheck->data,
'single_user_mode' => $singleUser?->data,
];
}
/**
* @return array
*/
private function getStaticConfiguration(): array
{
$list = EitherConfigKey::$static;
$return = [];
foreach ($list as $key) {
$return[$key] = config($key);
}
return $return;
}
/** /**
* This endpoint is documented at: * This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/configuration/getSingleConfiguration * https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/configuration/getSingleConfiguration
@@ -164,40 +200,4 @@ class ConfigurationController extends Controller
return response()->json(['data' => $data])->header('Content-Type', self::CONTENT_TYPE); return response()->json(['data' => $data])->header('Content-Type', self::CONTENT_TYPE);
} }
/**
* Get all config values.
*
* @return array
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getDynamicConfiguration(): array
{
$isDemoSite = app('fireflyconfig')->get('is_demo_site');
$updateCheck = app('fireflyconfig')->get('permission_update_check');
$lastCheck = app('fireflyconfig')->get('last_update_check');
$singleUser = app('fireflyconfig')->get('single_user_mode');
return [
'is_demo_site' => $isDemoSite?->data,
'permission_update_check' => null === $updateCheck ? null : (int)$updateCheck->data,
'last_update_check' => null === $lastCheck ? null : (int)$lastCheck->data,
'single_user_mode' => $singleUser?->data,
];
}
/**
* @return array
*/
private function getStaticConfiguration(): array
{
$list = EitherConfigKey::$static;
$return = [];
foreach ($list as $key) {
$return[$key] = config($key);
}
return $return;
}
} }

View File

@@ -82,6 +82,28 @@ class GenericRequest extends FormRequest
return $return; return $return;
} }
/**
*
*/
private function parseAccounts(): void
{
if (0 !== $this->accounts->count()) {
return;
}
$repository = app(AccountRepositoryInterface::class);
$repository->setUser(auth()->user());
$array = $this->get('accounts');
if (is_array($array)) {
foreach ($array as $accountId) {
$accountId = (int)$accountId;
$account = $repository->find($accountId);
if (null !== $account) {
$this->accounts->push($account);
}
}
}
}
/** /**
* @return Collection * @return Collection
*/ */
@@ -92,6 +114,28 @@ class GenericRequest extends FormRequest
return $this->bills; return $this->bills;
} }
/**
*
*/
private function parseBills(): void
{
if (0 !== $this->bills->count()) {
return;
}
$repository = app(BillRepositoryInterface::class);
$repository->setUser(auth()->user());
$array = $this->get('bills');
if (is_array($array)) {
foreach ($array as $billId) {
$billId = (int)$billId;
$bill = $repository->find($billId);
if (null !== $bill) {
$this->bills->push($bill);
}
}
}
}
/** /**
* @return Collection * @return Collection
*/ */
@@ -102,6 +146,28 @@ class GenericRequest extends FormRequest
return $this->budgets; return $this->budgets;
} }
/**
*
*/
private function parseBudgets(): void
{
if (0 !== $this->budgets->count()) {
return;
}
$repository = app(BudgetRepositoryInterface::class);
$repository->setUser(auth()->user());
$array = $this->get('budgets');
if (is_array($array)) {
foreach ($array as $budgetId) {
$budgetId = (int)$budgetId;
$budget = $repository->find($budgetId);
if (null !== $budget) {
$this->budgets->push($budget);
}
}
}
}
/** /**
* @return Collection * @return Collection
*/ */
@@ -112,6 +178,28 @@ class GenericRequest extends FormRequest
return $this->categories; return $this->categories;
} }
/**
*
*/
private function parseCategories(): void
{
if (0 !== $this->categories->count()) {
return;
}
$repository = app(CategoryRepositoryInterface::class);
$repository->setUser(auth()->user());
$array = $this->get('categories');
if (is_array($array)) {
foreach ($array as $categoryId) {
$categoryId = (int)$categoryId;
$category = $repository->find($categoryId);
if (null !== $category) {
$this->categories->push($category);
}
}
}
}
/** /**
* @return Carbon * @return Carbon
*/ */
@@ -180,114 +268,6 @@ class GenericRequest extends FormRequest
return $this->tags; return $this->tags;
} }
/**
* The rules that the incoming request must be matched against.
*
* @return array
*/
public function rules(): array
{
// this is cheating, but it works to initialize the collections.
$this->accounts = new Collection();
$this->budgets = new Collection();
$this->categories = new Collection();
$this->bills = new Collection();
$this->tags = new Collection();
return [
'start' => 'required|date',
'end' => 'required|date|after_or_equal:start',
];
}
/**
*
*/
private function parseAccounts(): void
{
if (0 !== $this->accounts->count()) {
return;
}
$repository = app(AccountRepositoryInterface::class);
$repository->setUser(auth()->user());
$array = $this->get('accounts');
if (is_array($array)) {
foreach ($array as $accountId) {
$accountId = (int)$accountId;
$account = $repository->find($accountId);
if (null !== $account) {
$this->accounts->push($account);
}
}
}
}
/**
*
*/
private function parseBills(): void
{
if (0 !== $this->bills->count()) {
return;
}
$repository = app(BillRepositoryInterface::class);
$repository->setUser(auth()->user());
$array = $this->get('bills');
if (is_array($array)) {
foreach ($array as $billId) {
$billId = (int)$billId;
$bill = $repository->find($billId);
if (null !== $bill) {
$this->bills->push($bill);
}
}
}
}
/**
*
*/
private function parseBudgets(): void
{
if (0 !== $this->budgets->count()) {
return;
}
$repository = app(BudgetRepositoryInterface::class);
$repository->setUser(auth()->user());
$array = $this->get('budgets');
if (is_array($array)) {
foreach ($array as $budgetId) {
$budgetId = (int)$budgetId;
$budget = $repository->find($budgetId);
if (null !== $budget) {
$this->budgets->push($budget);
}
}
}
}
/**
*
*/
private function parseCategories(): void
{
if (0 !== $this->categories->count()) {
return;
}
$repository = app(CategoryRepositoryInterface::class);
$repository->setUser(auth()->user());
$array = $this->get('categories');
if (is_array($array)) {
foreach ($array as $categoryId) {
$categoryId = (int)$categoryId;
$category = $repository->find($categoryId);
if (null !== $category) {
$this->categories->push($category);
}
}
}
}
/** /**
* *
*/ */
@@ -309,4 +289,24 @@ class GenericRequest extends FormRequest
} }
} }
} }
/**
* The rules that the incoming request must be matched against.
*
* @return array
*/
public function rules(): array
{
// this is cheating, but it works to initialize the collections.
$this->accounts = new Collection();
$this->budgets = new Collection();
$this->categories = new Collection();
$this->bills = new Collection();
$this->tags = new Collection();
return [
'start' => 'required|date',
'end' => 'required|date|after_or_equal:start',
];
}
} }

View File

@@ -73,6 +73,65 @@ class StoreRequest extends FormRequest
]; ];
} }
/**
* Returns the transaction data as it is found in the submitted data. It's a complex method according to code
* standards but it just has a lot of ??-statements because of the fields that may or may not exist.
*
* @return array
*/
private function getTransactionData(): array
{
$return = [];
// transaction data:
/** @var array|null $transactions */
$transactions = $this->get('transactions');
if (null === $transactions) {
return [];
}
/** @var array $transaction */
foreach ($transactions as $transaction) {
$return[] = $this->getSingleTransactionData($transaction);
}
return $return;
}
/**
* Returns the repetition data as it is found in the submitted data.
*
* @return array
*/
private function getRepetitionData(): array
{
$return = [];
// repetition data:
/** @var array|null $repetitions */
$repetitions = $this->get('repetitions');
if (null === $repetitions) {
return [];
}
/** @var array $repetition */
foreach ($repetitions as $repetition) {
$current = [];
if (array_key_exists('type', $repetition)) {
$current['type'] = $repetition['type'];
}
if (array_key_exists('moment', $repetition)) {
$current['moment'] = $repetition['moment'];
}
if (array_key_exists('skip', $repetition)) {
$current['skip'] = (int)$repetition['skip'];
}
if (array_key_exists('weekend', $repetition)) {
$current['weekend'] = (int)$repetition['weekend'];
}
$return[] = $current;
}
return $return;
}
/** /**
* The rules that the incoming request must be matched against. * The rules that the incoming request must be matched against.
* *
@@ -139,63 +198,4 @@ class StoreRequest extends FormRequest
} }
); );
} }
/**
* Returns the repetition data as it is found in the submitted data.
*
* @return array
*/
private function getRepetitionData(): array
{
$return = [];
// repetition data:
/** @var array|null $repetitions */
$repetitions = $this->get('repetitions');
if (null === $repetitions) {
return [];
}
/** @var array $repetition */
foreach ($repetitions as $repetition) {
$current = [];
if (array_key_exists('type', $repetition)) {
$current['type'] = $repetition['type'];
}
if (array_key_exists('moment', $repetition)) {
$current['moment'] = $repetition['moment'];
}
if (array_key_exists('skip', $repetition)) {
$current['skip'] = (int)$repetition['skip'];
}
if (array_key_exists('weekend', $repetition)) {
$current['weekend'] = (int)$repetition['weekend'];
}
$return[] = $current;
}
return $return;
}
/**
* Returns the transaction data as it is found in the submitted data. It's a complex method according to code
* standards but it just has a lot of ??-statements because of the fields that may or may not exist.
*
* @return array
*/
private function getTransactionData(): array
{
$return = [];
// transaction data:
/** @var array|null $transactions */
$transactions = $this->get('transactions');
if (null === $transactions) {
return [];
}
/** @var array $transaction */
foreach ($transactions as $transaction) {
$return[] = $this->getSingleTransactionData($transaction);
}
return $return;
}
} }

View File

@@ -80,6 +80,70 @@ class UpdateRequest extends FormRequest
return $return; return $return;
} }
/**
* Returns the repetition data as it is found in the submitted data.
*
* @return array|null
*/
private function getRepetitionData(): ?array
{
$return = [];
// repetition data:
/** @var array|null $repetitions */
$repetitions = $this->get('repetitions');
if (null === $repetitions) {
return null;
}
/** @var array $repetition */
foreach ($repetitions as $repetition) {
$current = [];
if (array_key_exists('type', $repetition)) {
$current['type'] = $repetition['type'];
}
if (array_key_exists('moment', $repetition)) {
$current['moment'] = (string)$repetition['moment'];
}
if (array_key_exists('skip', $repetition)) {
$current['skip'] = (int)$repetition['skip'];
}
if (array_key_exists('weekend', $repetition)) {
$current['weekend'] = (int)$repetition['weekend'];
}
$return[] = $current;
}
if (0 === count($return)) {
return null;
}
return $return;
}
/**
* Returns the transaction data as it is found in the submitted data. It's a complex method according to code
* standards but it just has a lot of ??-statements because of the fields that may or may not exist.
*
* @return array|null
*/
private function getTransactionData(): ?array
{
$return = [];
// transaction data:
/** @var array|null $transactions */
$transactions = $this->get('transactions');
if (null === $transactions) {
return null;
}
/** @var array $transaction */
foreach ($transactions as $transaction) {
$return[] = $this->getSingleTransactionData($transaction);
}
return $return;
}
/** /**
* The rules that the incoming request must be matched against. * The rules that the incoming request must be matched against.
* *
@@ -154,68 +218,4 @@ class UpdateRequest extends FormRequest
); );
} }
/**
* Returns the repetition data as it is found in the submitted data.
*
* @return array|null
*/
private function getRepetitionData(): ?array
{
$return = [];
// repetition data:
/** @var array|null $repetitions */
$repetitions = $this->get('repetitions');
if (null === $repetitions) {
return null;
}
/** @var array $repetition */
foreach ($repetitions as $repetition) {
$current = [];
if (array_key_exists('type', $repetition)) {
$current['type'] = $repetition['type'];
}
if (array_key_exists('moment', $repetition)) {
$current['moment'] = (string)$repetition['moment'];
}
if (array_key_exists('skip', $repetition)) {
$current['skip'] = (int)$repetition['skip'];
}
if (array_key_exists('weekend', $repetition)) {
$current['weekend'] = (int)$repetition['weekend'];
}
$return[] = $current;
}
if (0 === count($return)) {
return null;
}
return $return;
}
/**
* Returns the transaction data as it is found in the submitted data. It's a complex method according to code
* standards but it just has a lot of ??-statements because of the fields that may or may not exist.
*
* @return array|null
*/
private function getTransactionData(): ?array
{
$return = [];
// transaction data:
/** @var array|null $transactions */
$transactions = $this->get('transactions');
if (null === $transactions) {
return null;
}
/** @var array $transaction */
foreach ($transactions as $transaction) {
$return[] = $this->getSingleTransactionData($transaction);
}
return $return;
}
} }

View File

@@ -67,6 +67,48 @@ class StoreRequest extends FormRequest
return $data; return $data;
} }
/**
* @return array
*/
private function getRuleTriggers(): array
{
$triggers = $this->get('triggers');
$return = [];
if (is_array($triggers)) {
foreach ($triggers as $trigger) {
$return[] = [
'type' => $trigger['type'],
'value' => $trigger['value'],
'active' => $this->convertBoolean((string)($trigger['active'] ?? 'true')),
'stop_processing' => $this->convertBoolean((string)($trigger['stop_processing'] ?? 'false')),
];
}
}
return $return;
}
/**
* @return array
*/
private function getRuleActions(): array
{
$actions = $this->get('actions');
$return = [];
if (is_array($actions)) {
foreach ($actions as $action) {
$return[] = [
'type' => $action['type'],
'value' => $action['value'],
'active' => $this->convertBoolean((string)($action['active'] ?? 'true')),
'stop_processing' => $this->convertBoolean((string)($action['stop_processing'] ?? 'false')),
];
}
}
return $return;
}
/** /**
* The rules that the incoming request must be matched against. * The rules that the incoming request must be matched against.
* *
@@ -120,6 +162,21 @@ class StoreRequest extends FormRequest
); );
} }
/**
* Adds an error to the validator when there are no triggers in the array of data.
*
* @param Validator $validator
*/
protected function atLeastOneTrigger(Validator $validator): void
{
$data = $validator->getData();
$triggers = $data['triggers'] ?? [];
// need at least one trigger
if (!is_countable($triggers) || 0 === count($triggers)) {
$validator->errors()->add('title', (string)trans('validation.at_least_one_trigger'));
}
}
/** /**
* Adds an error to the validator when there are no repetitions in the array of data. * Adds an error to the validator when there are no repetitions in the array of data.
* *
@@ -135,35 +192,6 @@ class StoreRequest extends FormRequest
} }
} }
/**
* Adds an error to the validator when there are no ACTIVE actions in the array of data.
*
* @param Validator $validator
*/
protected function atLeastOneActiveAction(Validator $validator): void
{
$data = $validator->getData();
$actions = $data['actions'] ?? [];
// need at least one trigger
if (!is_countable($actions) || 0 === count($actions)) {
return;
}
$allInactive = true;
$inactiveIndex = 0;
foreach ($actions as $index => $action) {
$active = array_key_exists('active', $action) ? $action['active'] : true; // assume true
if (true === $active) {
$allInactive = false;
}
if (false === $active) {
$inactiveIndex = $index;
}
}
if (true === $allInactive) {
$validator->errors()->add(sprintf('actions.%d.active', $inactiveIndex), (string)trans('validation.at_least_one_active_action'));
}
}
/** /**
* Adds an error to the validator when there are no ACTIVE triggers in the array of data. * Adds an error to the validator when there are no ACTIVE triggers in the array of data.
* *
@@ -194,59 +222,31 @@ class StoreRequest extends FormRequest
} }
/** /**
* Adds an error to the validator when there are no triggers in the array of data. * Adds an error to the validator when there are no ACTIVE actions in the array of data.
* *
* @param Validator $validator * @param Validator $validator
*/ */
protected function atLeastOneTrigger(Validator $validator): void protected function atLeastOneActiveAction(Validator $validator): void
{ {
$data = $validator->getData(); $data = $validator->getData();
$triggers = $data['triggers'] ?? []; $actions = $data['actions'] ?? [];
// need at least one trigger // need at least one trigger
if (!is_countable($triggers) || 0 === count($triggers)) { if (!is_countable($actions) || 0 === count($actions)) {
$validator->errors()->add('title', (string)trans('validation.at_least_one_trigger')); return;
}
$allInactive = true;
$inactiveIndex = 0;
foreach ($actions as $index => $action) {
$active = array_key_exists('active', $action) ? $action['active'] : true; // assume true
if (true === $active) {
$allInactive = false;
}
if (false === $active) {
$inactiveIndex = $index;
} }
} }
if (true === $allInactive) {
/** $validator->errors()->add(sprintf('actions.%d.active', $inactiveIndex), (string)trans('validation.at_least_one_active_action'));
* @return array
*/
private function getRuleActions(): array
{
$actions = $this->get('actions');
$return = [];
if (is_array($actions)) {
foreach ($actions as $action) {
$return[] = [
'type' => $action['type'],
'value' => $action['value'],
'active' => $this->convertBoolean((string)($action['active'] ?? 'true')),
'stop_processing' => $this->convertBoolean((string)($action['stop_processing'] ?? 'false')),
];
} }
} }
return $return;
}
/**
* @return array
*/
private function getRuleTriggers(): array
{
$triggers = $this->get('triggers');
$return = [];
if (is_array($triggers)) {
foreach ($triggers as $trigger) {
$return[] = [
'type' => $trigger['type'],
'value' => $trigger['value'],
'active' => $this->convertBoolean((string)($trigger['active'] ?? 'true')),
'stop_processing' => $this->convertBoolean((string)($trigger['stop_processing'] ?? 'false')),
];
}
}
return $return;
}
} }

View File

@@ -52,24 +52,11 @@ class TestRequest extends FormRequest
} }
/** /**
* @return array * @return int
*/ */
public function rules(): array private function getPage(): int
{ {
return [ return 0 === (int)$this->query('page') ? 1 : (int)$this->query('page');
'start' => 'date',
'end' => 'date|after_or_equal:start',
'accounts' => '',
'accounts.*' => 'required|exists:accounts,id|belongsToUser:accounts',
];
}
/**
* @return array
*/
private function getAccounts(): array
{
return $this->get('accounts');
} }
/** /**
@@ -83,10 +70,23 @@ class TestRequest extends FormRequest
} }
/** /**
* @return int * @return array
*/ */
private function getPage(): int private function getAccounts(): array
{ {
return 0 === (int)$this->query('page') ? 1 : (int)$this->query('page'); return $this->get('accounts');
}
/**
* @return array
*/
public function rules(): array
{
return [
'start' => 'date',
'end' => 'date|after_or_equal:start',
'accounts' => '',
'accounts.*' => 'required|exists:accounts,id|belongsToUser:accounts',
];
} }
} }

View File

@@ -50,16 +50,13 @@ class TriggerRequest extends FormRequest
} }
/** /**
* @return array * @param string $field
*
* @return Carbon|null
*/ */
public function rules(): array private function getDate(string $field): ?Carbon
{ {
return [ return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($this->query($field), 0, 10));
'start' => 'date',
'end' => 'date|after_or_equal:start',
'accounts' => '',
'accounts.*' => 'exists:accounts,id|belongsToUser:accounts',
];
} }
/** /**
@@ -71,12 +68,15 @@ class TriggerRequest extends FormRequest
} }
/** /**
* @param string $field * @return array
*
* @return Carbon|null
*/ */
private function getDate(string $field): ?Carbon public function rules(): array
{ {
return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($this->query($field), 0, 10)); return [
'start' => 'date',
'end' => 'date|after_or_equal:start',
'accounts' => '',
'accounts.*' => 'exists:accounts,id|belongsToUser:accounts',
];
} }
} }

View File

@@ -73,6 +73,56 @@ class UpdateRequest extends FormRequest
return $return; return $return;
} }
/**
* @return array|null
*/
private function getRuleTriggers(): ?array
{
if (!$this->has('triggers')) {
return null;
}
$triggers = $this->get('triggers');
$return = [];
if (is_array($triggers)) {
foreach ($triggers as $trigger) {
$active = array_key_exists('active', $trigger) ? $trigger['active'] : true;
$stopProcessing = array_key_exists('stop_processing', $trigger) ? $trigger['stop_processing'] : false;
$return[] = [
'type' => $trigger['type'],
'value' => $trigger['value'],
'active' => $active,
'stop_processing' => $stopProcessing,
];
}
}
return $return;
}
/**
* @return array|null
*/
private function getRuleActions(): ?array
{
if (!$this->has('actions')) {
return null;
}
$actions = $this->get('actions');
$return = [];
if (is_array($actions)) {
foreach ($actions as $action) {
$return[] = [
'type' => $action['type'],
'value' => $action['value'],
'active' => $this->convertBoolean((string)($action['active'] ?? 'false')),
'stop_processing' => $this->convertBoolean((string)($action['stop_processing'] ?? 'false')),
];
}
}
return $return;
}
/** /**
* The rules that the incoming request must be matched against. * The rules that the incoming request must be matched against.
* *
@@ -130,21 +180,6 @@ class UpdateRequest extends FormRequest
); );
} }
/**
* Adds an error to the validator when there are no repetitions in the array of data.
*
* @param Validator $validator
*/
protected function atLeastOneAction(Validator $validator): void
{
$data = $validator->getData();
$actions = $data['actions'] ?? null;
// need at least one action
if (is_array($actions) && 0 === count($actions)) {
$validator->errors()->add('title', (string)trans('validation.at_least_one_action'));
}
}
/** /**
* Adds an error to the validator when there are no repetitions in the array of data. * Adds an error to the validator when there are no repetitions in the array of data.
* *
@@ -160,36 +195,6 @@ class UpdateRequest extends FormRequest
} }
} }
/**
* Adds an error to the validator when there are no repetitions in the array of data.
*
* @param Validator $validator
*/
protected function atLeastOneValidAction(Validator $validator): void
{
$data = $validator->getData();
$actions = $data['actions'] ?? [];
$allInactive = true;
$inactiveIndex = 0;
// need at least one action
if (is_array($actions) && 0 === count($actions)) {
return;
}
foreach ($actions as $index => $action) {
$active = array_key_exists('active', $action) ? $action['active'] : true; // assume true
if (true === $active) {
$allInactive = false;
}
if (false === $active) {
$inactiveIndex = $index;
}
}
if (true === $allInactive) {
$validator->errors()->add(sprintf('actions.%d.active', $inactiveIndex), (string)trans('validation.at_least_one_active_action'));
}
}
/** /**
* Adds an error to the validator when there are no repetitions in the array of data. * Adds an error to the validator when there are no repetitions in the array of data.
* *
@@ -220,52 +225,47 @@ class UpdateRequest extends FormRequest
} }
/** /**
* @return array|null * Adds an error to the validator when there are no repetitions in the array of data.
*
* @param Validator $validator
*/ */
private function getRuleActions(): ?array protected function atLeastOneAction(Validator $validator): void
{ {
if (!$this->has('actions')) { $data = $validator->getData();
return null; $actions = $data['actions'] ?? null;
// need at least one action
if (is_array($actions) && 0 === count($actions)) {
$validator->errors()->add('title', (string)trans('validation.at_least_one_action'));
} }
$actions = $this->get('actions');
$return = [];
if (is_array($actions)) {
foreach ($actions as $action) {
$return[] = [
'type' => $action['type'],
'value' => $action['value'],
'active' => $this->convertBoolean((string)($action['active'] ?? 'false')),
'stop_processing' => $this->convertBoolean((string)($action['stop_processing'] ?? 'false')),
];
}
}
return $return;
} }
/** /**
* @return array|null * Adds an error to the validator when there are no repetitions in the array of data.
*
* @param Validator $validator
*/ */
private function getRuleTriggers(): ?array protected function atLeastOneValidAction(Validator $validator): void
{ {
if (!$this->has('triggers')) { $data = $validator->getData();
return null; $actions = $data['actions'] ?? [];
} $allInactive = true;
$triggers = $this->get('triggers'); $inactiveIndex = 0;
$return = []; // need at least one action
if (is_array($triggers)) { if (is_array($actions) && 0 === count($actions)) {
foreach ($triggers as $trigger) { return;
$active = array_key_exists('active', $trigger) ? $trigger['active'] : true;
$stopProcessing = array_key_exists('stop_processing', $trigger) ? $trigger['stop_processing'] : false;
$return[] = [
'type' => $trigger['type'],
'value' => $trigger['value'],
'active' => $active,
'stop_processing' => $stopProcessing,
];
}
} }
return $return; foreach ($actions as $index => $action) {
$active = array_key_exists('active', $action) ? $action['active'] : true; // assume true
if (true === $active) {
$allInactive = false;
}
if (false === $active) {
$inactiveIndex = $index;
}
}
if (true === $allInactive) {
$validator->errors()->add(sprintf('actions.%d.active', $inactiveIndex), (string)trans('validation.at_least_one_active_action'));
}
} }
} }

View File

@@ -50,16 +50,13 @@ class TestRequest extends FormRequest
} }
/** /**
* @return array * @param string $field
*
* @return Carbon|null
*/ */
public function rules(): array private function getDate(string $field): ?Carbon
{ {
return [ return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', $this->query($field));
'start' => 'date',
'end' => 'date|after_or_equal:start',
'accounts' => '',
'accounts.*' => 'exists:accounts,id|belongsToUser:accounts',
];
} }
/** /**
@@ -71,12 +68,15 @@ class TestRequest extends FormRequest
} }
/** /**
* @param string $field * @return array
*
* @return Carbon|null
*/ */
private function getDate(string $field): ?Carbon public function rules(): array
{ {
return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', $this->query($field)); return [
'start' => 'date',
'end' => 'date|after_or_equal:start',
'accounts' => '',
'accounts.*' => 'exists:accounts,id|belongsToUser:accounts',
];
} }
} }

View File

@@ -50,14 +50,13 @@ class TriggerRequest extends FormRequest
} }
/** /**
* @return array * @param string $field
*
* @return Carbon|null
*/ */
public function rules(): array private function getDate(string $field): ?Carbon
{ {
return [ return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', $this->query($field));
'start' => 'date',
'end' => 'date|after_or_equal:start',
];
} }
/** /**
@@ -69,12 +68,13 @@ class TriggerRequest extends FormRequest
} }
/** /**
* @param string $field * @return array
*
* @return Carbon|null
*/ */
private function getDate(string $field): ?Carbon public function rules(): array
{ {
return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', $this->query($field)); return [
'start' => 'date',
'end' => 'date|after_or_equal:start',
];
} }
} }

View File

@@ -69,6 +69,103 @@ class StoreRequest extends FormRequest
// TODO include location and ability to process it. // TODO include location and ability to process it.
} }
/**
* Get transaction data.
*
* @return array
*/
private function getTransactionData(): array
{
$return = [];
/**
* @var array $transaction
*/
foreach ($this->get('transactions') as $transaction) {
$object = new NullArrayObject($transaction);
$return[] = [
'type' => $this->clearString($object['type'], false),
'date' => $this->dateFromValue($object['date']),
'order' => $this->integerFromValue((string)$object['order']),
'currency_id' => $this->integerFromValue((string)$object['currency_id']),
'currency_code' => $this->clearString((string)$object['currency_code'], false),
// foreign currency info:
'foreign_currency_id' => $this->integerFromValue((string)$object['foreign_currency_id']),
'foreign_currency_code' => $this->clearString((string)$object['foreign_currency_code'], false),
// amount and foreign amount. Cannot be 0.
'amount' => $this->clearString((string)$object['amount'], false),
'foreign_amount' => $this->clearString((string)$object['foreign_amount'], false),
// description.
'description' => $this->clearString($object['description'], false),
// source of transaction. If everything is null, assume cash account.
'source_id' => $this->integerFromValue((string)$object['source_id']),
'source_name' => $this->clearString((string)$object['source_name'], false),
'source_iban' => $this->clearString((string)$object['source_iban'], false),
'source_number' => $this->clearString((string)$object['source_number'], false),
'source_bic' => $this->clearString((string)$object['source_bic'], false),
// destination of transaction. If everything is null, assume cash account.
'destination_id' => $this->integerFromValue((string)$object['destination_id']),
'destination_name' => $this->clearString((string)$object['destination_name'], false),
'destination_iban' => $this->clearString((string)$object['destination_iban'], false),
'destination_number' => $this->clearString((string)$object['destination_number'], false),
'destination_bic' => $this->clearString((string)$object['destination_bic'], false),
// budget info
'budget_id' => $this->integerFromValue((string)$object['budget_id']),
'budget_name' => $this->clearString((string)$object['budget_name'], false),
// category info
'category_id' => $this->integerFromValue((string)$object['category_id']),
'category_name' => $this->clearString((string)$object['category_name'], false),
// journal bill reference. Optional. Will only work for withdrawals
'bill_id' => $this->integerFromValue((string)$object['bill_id']),
'bill_name' => $this->clearString((string)$object['bill_name'], false),
// piggy bank reference. Optional. Will only work for transfers
'piggy_bank_id' => $this->integerFromValue((string)$object['piggy_bank_id']),
'piggy_bank_name' => $this->clearString((string)$object['piggy_bank_name'], false),
// some other interesting properties
'reconciled' => $this->convertBoolean((string)$object['reconciled']),
'notes' => $this->clearString((string)$object['notes']),
'tags' => $this->arrayFromValue($object['tags']),
// all custom fields:
'internal_reference' => $this->clearString((string)$object['internal_reference'], false),
'external_id' => $this->clearString((string)$object['external_id'], false),
'original_source' => sprintf('ff3-v%s|api-v%s', config('firefly.version'), config('firefly.api_version')),
'recurrence_id' => $this->integerFromValue($object['recurrence_id']),
'bunq_payment_id' => $this->clearString((string)$object['bunq_payment_id'], false),
'external_url' => $this->clearString((string)$object['external_url'], false),
'sepa_cc' => $this->clearString((string)$object['sepa_cc'], false),
'sepa_ct_op' => $this->clearString((string)$object['sepa_ct_op'], false),
'sepa_ct_id' => $this->clearString((string)$object['sepa_ct_id'], false),
'sepa_db' => $this->clearString((string)$object['sepa_db'], false),
'sepa_country' => $this->clearString((string)$object['sepa_country'], false),
'sepa_ep' => $this->clearString((string)$object['sepa_ep'], false),
'sepa_ci' => $this->clearString((string)$object['sepa_ci'], false),
'sepa_batch_id' => $this->clearString((string)$object['sepa_batch_id'], false),
// custom date fields. Must be Carbon objects. Presence is optional.
'interest_date' => $this->dateFromValue($object['interest_date']),
'book_date' => $this->dateFromValue($object['book_date']),
'process_date' => $this->dateFromValue($object['process_date']),
'due_date' => $this->dateFromValue($object['due_date']),
'payment_date' => $this->dateFromValue($object['payment_date']),
'invoice_date' => $this->dateFromValue($object['invoice_date']),
];
}
return $return;
}
/** /**
* The rules that the incoming request must be matched against. * The rules that the incoming request must be matched against.
* *
@@ -197,101 +294,4 @@ class StoreRequest extends FormRequest
} }
); );
} }
/**
* Get transaction data.
*
* @return array
*/
private function getTransactionData(): array
{
$return = [];
/**
* @var array $transaction
*/
foreach ($this->get('transactions') as $transaction) {
$object = new NullArrayObject($transaction);
$return[] = [
'type' => $this->clearString($object['type'], false),
'date' => $this->dateFromValue($object['date']),
'order' => $this->integerFromValue((string)$object['order']),
'currency_id' => $this->integerFromValue((string)$object['currency_id']),
'currency_code' => $this->clearString((string)$object['currency_code'], false),
// foreign currency info:
'foreign_currency_id' => $this->integerFromValue((string)$object['foreign_currency_id']),
'foreign_currency_code' => $this->clearString((string)$object['foreign_currency_code'], false),
// amount and foreign amount. Cannot be 0.
'amount' => $this->clearString((string)$object['amount'], false),
'foreign_amount' => $this->clearString((string)$object['foreign_amount'], false),
// description.
'description' => $this->clearString($object['description'], false),
// source of transaction. If everything is null, assume cash account.
'source_id' => $this->integerFromValue((string)$object['source_id']),
'source_name' => $this->clearString((string)$object['source_name'], false),
'source_iban' => $this->clearString((string)$object['source_iban'], false),
'source_number' => $this->clearString((string)$object['source_number'], false),
'source_bic' => $this->clearString((string)$object['source_bic'], false),
// destination of transaction. If everything is null, assume cash account.
'destination_id' => $this->integerFromValue((string)$object['destination_id']),
'destination_name' => $this->clearString((string)$object['destination_name'], false),
'destination_iban' => $this->clearString((string)$object['destination_iban'], false),
'destination_number' => $this->clearString((string)$object['destination_number'], false),
'destination_bic' => $this->clearString((string)$object['destination_bic'], false),
// budget info
'budget_id' => $this->integerFromValue((string)$object['budget_id']),
'budget_name' => $this->clearString((string)$object['budget_name'], false),
// category info
'category_id' => $this->integerFromValue((string)$object['category_id']),
'category_name' => $this->clearString((string)$object['category_name'], false),
// journal bill reference. Optional. Will only work for withdrawals
'bill_id' => $this->integerFromValue((string)$object['bill_id']),
'bill_name' => $this->clearString((string)$object['bill_name'], false),
// piggy bank reference. Optional. Will only work for transfers
'piggy_bank_id' => $this->integerFromValue((string)$object['piggy_bank_id']),
'piggy_bank_name' => $this->clearString((string)$object['piggy_bank_name'], false),
// some other interesting properties
'reconciled' => $this->convertBoolean((string)$object['reconciled']),
'notes' => $this->clearString((string)$object['notes']),
'tags' => $this->arrayFromValue($object['tags']),
// all custom fields:
'internal_reference' => $this->clearString((string)$object['internal_reference'], false),
'external_id' => $this->clearString((string)$object['external_id'], false),
'original_source' => sprintf('ff3-v%s|api-v%s', config('firefly.version'), config('firefly.api_version')),
'recurrence_id' => $this->integerFromValue($object['recurrence_id']),
'bunq_payment_id' => $this->clearString((string)$object['bunq_payment_id'], false),
'external_url' => $this->clearString((string)$object['external_url'], false),
'sepa_cc' => $this->clearString((string)$object['sepa_cc'], false),
'sepa_ct_op' => $this->clearString((string)$object['sepa_ct_op'], false),
'sepa_ct_id' => $this->clearString((string)$object['sepa_ct_id'], false),
'sepa_db' => $this->clearString((string)$object['sepa_db'], false),
'sepa_country' => $this->clearString((string)$object['sepa_country'], false),
'sepa_ep' => $this->clearString((string)$object['sepa_ep'], false),
'sepa_ci' => $this->clearString((string)$object['sepa_ci'], false),
'sepa_batch_id' => $this->clearString((string)$object['sepa_batch_id'], false),
// custom date fields. Must be Carbon objects. Presence is optional.
'interest_date' => $this->dateFromValue($object['interest_date']),
'book_date' => $this->dateFromValue($object['book_date']),
'process_date' => $this->dateFromValue($object['process_date']),
'due_date' => $this->dateFromValue($object['due_date']),
'payment_date' => $this->dateFromValue($object['payment_date']),
'invoice_date' => $this->dateFromValue($object['invoice_date']),
];
}
return $return;
}
} }

View File

@@ -32,7 +32,6 @@ use FireflyIII\Models\AccountType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Administration\Account\AccountRepositoryInterface as AdminAccountRepositoryInterface; use FireflyIII\Repositories\Administration\Account\AccountRepositoryInterface as AdminAccountRepositoryInterface;
use FireflyIII\Support\Http\Api\AccountFilter; use FireflyIII\Support\Http\Api\AccountFilter;
use FireflyIII\User;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use JsonException; use JsonException;

View File

@@ -64,54 +64,6 @@ class Controller extends BaseController
} }
} }
/**
* @param string $key
* @param LengthAwarePaginator $paginator
* @param AbstractTransformer $transformer
*
* @return array
*/
final protected function jsonApiList(string $key, LengthAwarePaginator $paginator, AbstractTransformer $transformer): array
{
$manager = new Manager();
$baseUrl = request()->getSchemeAndHttpHost().'/api/v2';
$manager->setSerializer(new JsonApiSerializer($baseUrl));
$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.
$transformer->collectMetaData($objects);
$resource = new FractalCollection($objects, $transformer, $key);
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return $manager->createData($resource)->toArray();
}
/**
* Returns a JSON API object and returns it.
*
* @param string $key
* @param Model $object
* @param AbstractTransformer $transformer
*
* @return array
*/
final protected function jsonApiObject(string $key, Model $object, AbstractTransformer $transformer): array
{
// create some objects:
$manager = new Manager();
$baseUrl = request()->getSchemeAndHttpHost().'/api/v2';
$manager->setSerializer(new JsonApiSerializer($baseUrl));
$transformer->collectMetaData(new Collection([$object]));
$resource = new Item($object, $transformer, $key);
return $manager->createData($resource)->toArray();
}
/** /**
* TODO duplicate from V1 controller * TODO duplicate from V1 controller
* Method to grab all parameters from the URL. * Method to grab all parameters from the URL.
@@ -179,4 +131,52 @@ class Controller extends BaseController
return $bag; return $bag;
} }
/**
* @param string $key
* @param LengthAwarePaginator $paginator
* @param AbstractTransformer $transformer
*
* @return array
*/
final protected function jsonApiList(string $key, LengthAwarePaginator $paginator, AbstractTransformer $transformer): array
{
$manager = new Manager();
$baseUrl = request()->getSchemeAndHttpHost() . '/api/v2';
$manager->setSerializer(new JsonApiSerializer($baseUrl));
$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.
$transformer->collectMetaData($objects);
$resource = new FractalCollection($objects, $transformer, $key);
$resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
return $manager->createData($resource)->toArray();
}
/**
* Returns a JSON API object and returns it.
*
* @param string $key
* @param Model $object
* @param AbstractTransformer $transformer
*
* @return array
*/
final protected function jsonApiObject(string $key, Model $object, AbstractTransformer $transformer): array
{
// create some objects:
$manager = new Manager();
$baseUrl = request()->getSchemeAndHttpHost() . '/api/v2';
$manager->setSerializer(new JsonApiSerializer($baseUrl));
$transformer->collectMetaData(new Collection([$object]));
$resource = new Item($object, $transformer, $key);
return $manager->createData($resource)->toArray();
}
} }

View File

@@ -174,6 +174,26 @@ class CorrectAmounts extends Command
$this->friendlyInfo(sprintf('Corrected %d currency exchange rate(s).', $count)); $this->friendlyInfo(sprintf('Corrected %d currency exchange rate(s).', $count));
} }
/**
* @return void
*/
private function fixRepetitions(): void
{
$set = PiggyBankRepetition::where('currentamount', '<', 0)->get();
$count = $set->count();
if (0 === $count) {
$this->friendlyPositive('All piggy bank repetition amounts are positive.');
return;
}
/** @var PiggyBankRepetition $item */
foreach ($set as $item) {
$item->currentamount = app('steam')->positive((string)$item->currentamount);
$item->save();
}
$this->friendlyInfo(sprintf('Corrected %d piggy bank repetition amount(s).', $count));
}
/** /**
* @return void * @return void
*/ */
@@ -217,26 +237,6 @@ class CorrectAmounts extends Command
$this->friendlyInfo(sprintf('Corrected %d recurring transaction amount(s).', $count)); $this->friendlyInfo(sprintf('Corrected %d recurring transaction amount(s).', $count));
} }
/**
* @return void
*/
private function fixRepetitions(): void
{
$set = PiggyBankRepetition::where('currentamount', '<', 0)->get();
$count = $set->count();
if (0 === $count) {
$this->friendlyPositive('All piggy bank repetition amounts are positive.');
return;
}
/** @var PiggyBankRepetition $item */
foreach ($set as $item) {
$item->currentamount = app('steam')->positive((string)$item->currentamount);
$item->save();
}
$this->friendlyInfo(sprintf('Corrected %d piggy bank repetition amount(s).', $count));
}
/** /**
* @return void * @return void
*/ */

View File

@@ -71,6 +71,17 @@ class CorrectOpeningBalanceCurrencies extends Command
return 0; return 0;
} }
/**
* @return Collection
*/
private function getJournals(): Collection
{
/** @var Collection */
return TransactionJournal::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->whereNull('transaction_journals.deleted_at')
->where('transaction_types.type', TransactionType::OPENING_BALANCE)->get(['transaction_journals.*']);
}
/** /**
* @param TransactionJournal $journal * @param TransactionJournal $journal
* *
@@ -112,31 +123,6 @@ class CorrectOpeningBalanceCurrencies extends Command
return null; return null;
} }
/**
* @param Account $account
*
* @return TransactionCurrency
*/
private function getCurrency(Account $account): TransactionCurrency
{
/** @var AccountRepositoryInterface $repos */
$repos = app(AccountRepositoryInterface::class);
$repos->setUser($account->user);
return $repos->getAccountCurrency($account) ?? app('amount')->getDefaultCurrencyByUser($account->user);
}
/**
* @return Collection
*/
private function getJournals(): Collection
{
/** @var Collection */
return TransactionJournal::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->whereNull('transaction_journals.deleted_at')
->where('transaction_types.type', TransactionType::OPENING_BALANCE)->get(['transaction_journals.*']);
}
/** /**
* @param Account $account * @param Account $account
* @param TransactionJournal $journal * @param TransactionJournal $journal
@@ -163,4 +149,18 @@ class CorrectOpeningBalanceCurrencies extends Command
return $count; return $count;
} }
/**
* @param Account $account
*
* @return TransactionCurrency
*/
private function getCurrency(Account $account): TransactionCurrency
{
/** @var AccountRepositoryInterface $repos */
$repos = app(AccountRepositoryInterface::class);
$repos->setUser($account->user);
return $repos->getAccountCurrency($account) ?? app('amount')->getDefaultCurrencyByUser($account->user);
}
} }

View File

@@ -64,30 +64,6 @@ class DeleteEmptyJournals extends Command
return 0; return 0;
} }
private function deleteEmptyJournals(): void
{
$count = 0;
$set = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->groupBy('transaction_journals.id')
->whereNull('transactions.transaction_journal_id')
->get(['transaction_journals.id']);
foreach ($set as $entry) {
try {
TransactionJournal::find($entry->id)->delete();
} catch (QueryException $e) {
Log::info(sprintf('Could not delete entry: %s', $e->getMessage()));
}
$this->friendlyInfo(sprintf('Deleted empty transaction journal #%d', $entry->id));
++$count;
}
if (0 === $count) {
$this->friendlyPositive('No empty transaction journals.');
}
}
/** /**
* Delete transactions and their journals if they have an uneven number of transactions. * Delete transactions and their journals if they have an uneven number of transactions.
*/ */
@@ -120,4 +96,28 @@ class DeleteEmptyJournals extends Command
$this->friendlyPositive('No uneven transaction journals.'); $this->friendlyPositive('No uneven transaction journals.');
} }
} }
private function deleteEmptyJournals(): void
{
$count = 0;
$set = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->groupBy('transaction_journals.id')
->whereNull('transactions.transaction_journal_id')
->get(['transaction_journals.id']);
foreach ($set as $entry) {
try {
TransactionJournal::find($entry->id)->delete();
} catch (QueryException $e) {
Log::info(sprintf('Could not delete entry: %s', $e->getMessage()));
}
$this->friendlyInfo(sprintf('Deleted empty transaction journal #%d', $entry->id));
++$count;
}
if (0 === $count) {
$this->friendlyPositive('No empty transaction journals.');
}
}
} }

View File

@@ -65,38 +65,6 @@ class DeleteOrphanedTransactions extends Command
return 0; return 0;
} }
/**
*
*/
private function deleteFromOrphanedAccounts(): void
{
$set
= Transaction::leftJoin('accounts', 'transactions.account_id', '=', 'accounts.id')
->whereNotNull('accounts.deleted_at')
->get(['transactions.*']);
$count = 0;
/** @var Transaction $transaction */
foreach ($set as $transaction) {
// delete journals
$journal = TransactionJournal::find((int)$transaction->transaction_journal_id);
if ($journal) {
$journal->delete();
}
Transaction::where('transaction_journal_id', (int)$transaction->transaction_journal_id)->delete();
$this->friendlyWarning(
sprintf(
'Deleted transaction journal #%d because account #%d was already deleted.',
$transaction->transaction_journal_id,
$transaction->account_id
)
);
$count++;
}
if (0 === $count) {
$this->friendlyPositive('No orphaned accounts.');
}
}
private function deleteOrphanedJournals(): void private function deleteOrphanedJournals(): void
{ {
$set = TransactionJournal::leftJoin('transaction_groups', 'transaction_journals.transaction_group_id', 'transaction_groups.id') $set = TransactionJournal::leftJoin('transaction_groups', 'transaction_journals.transaction_group_id', 'transaction_groups.id')
@@ -159,4 +127,36 @@ class DeleteOrphanedTransactions extends Command
$this->friendlyPositive('No orphaned transactions.'); $this->friendlyPositive('No orphaned transactions.');
} }
} }
/**
*
*/
private function deleteFromOrphanedAccounts(): void
{
$set
= Transaction::leftJoin('accounts', 'transactions.account_id', '=', 'accounts.id')
->whereNotNull('accounts.deleted_at')
->get(['transactions.*']);
$count = 0;
/** @var Transaction $transaction */
foreach ($set as $transaction) {
// delete journals
$journal = TransactionJournal::find((int)$transaction->transaction_journal_id);
if ($journal) {
$journal->delete();
}
Transaction::where('transaction_journal_id', (int)$transaction->transaction_journal_id)->delete();
$this->friendlyWarning(
sprintf(
'Deleted transaction journal #%d because account #%d was already deleted.',
$transaction->transaction_journal_id,
$transaction->account_id
)
);
$count++;
}
if (0 === $count) {
$this->friendlyPositive('No orphaned accounts.');
}
}
} }

View File

@@ -31,7 +31,6 @@ use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/** /**
* Class EnableCurrencies * Class EnableCurrencies

View File

@@ -31,8 +31,8 @@ use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType; use FireflyIII\Models\TransactionType;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use JsonException;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use JsonException;
/** /**
* Class FixAccountTypes * Class FixAccountTypes
@@ -73,6 +73,79 @@ class FixAccountTypes extends Command
return 0; return 0;
} }
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*
*/
private function stupidLaravel(): void
{
$this->count = 0;
}
/**
* @param TransactionJournal $journal
*
* @throws FireflyException
*/
private function inspectJournal(TransactionJournal $journal): void
{
$transactions = $journal->transactions()->count();
if (2 !== $transactions) {
Log::debug(sprintf('Journal has %d transactions, so can\'t fix.', $transactions));
$this->friendlyError(sprintf('Cannot inspect transaction journal #%d because it has %d transaction(s) instead of 2.', $journal->id, $transactions));
return;
}
$type = $journal->transactionType->type;
$sourceTransaction = $this->getSourceTransaction($journal);
$destTransaction = $this->getDestinationTransaction($journal);
$sourceAccount = $sourceTransaction->account;
$sourceAccountType = $sourceAccount->accountType->type;
$destAccount = $destTransaction->account;
$destAccountType = $destAccount->accountType->type;
if (!array_key_exists($type, $this->expected)) {
Log::info(sprintf('No source/destination info for transaction type %s.', $type));
$this->friendlyError(sprintf('No source/destination info for transaction type %s.', $type));
return;
}
if (!array_key_exists($sourceAccountType, $this->expected[$type])) {
Log::debug(sprintf('Going to fix journal #%d', $journal->id));
$this->fixJournal($journal, $type, $sourceTransaction, $destTransaction);
return;
}
$expectedTypes = $this->expected[$type][$sourceAccountType];
if (!in_array($destAccountType, $expectedTypes, true)) {
Log::debug(sprintf('Going to fix journal #%d', $journal->id));
$this->fixJournal($journal, $type, $sourceTransaction, $destTransaction);
}
}
/**
* @param TransactionJournal $journal
*
* @return Transaction
*/
private function getSourceTransaction(TransactionJournal $journal): Transaction
{
return $journal->transactions->firstWhere('amount', '<', 0);
}
/**
* @param TransactionJournal $journal
*
* @return Transaction
*/
private function getDestinationTransaction(TransactionJournal $journal): Transaction
{
return $journal->transactions->firstWhere('amount', '>', 0);
}
/** /**
* @param TransactionJournal $journal * @param TransactionJournal $journal
* @param string $type * @param string $type
@@ -167,77 +240,4 @@ class FixAccountTypes extends Command
break; break;
} }
} }
/**
* @param TransactionJournal $journal
*
* @return Transaction
*/
private function getDestinationTransaction(TransactionJournal $journal): Transaction
{
return $journal->transactions->firstWhere('amount', '>', 0);
}
/**
* @param TransactionJournal $journal
*
* @return Transaction
*/
private function getSourceTransaction(TransactionJournal $journal): Transaction
{
return $journal->transactions->firstWhere('amount', '<', 0);
}
/**
* @param TransactionJournal $journal
*
* @throws FireflyException
*/
private function inspectJournal(TransactionJournal $journal): void
{
$transactions = $journal->transactions()->count();
if (2 !== $transactions) {
Log::debug(sprintf('Journal has %d transactions, so can\'t fix.', $transactions));
$this->friendlyError(sprintf('Cannot inspect transaction journal #%d because it has %d transaction(s) instead of 2.', $journal->id, $transactions));
return;
}
$type = $journal->transactionType->type;
$sourceTransaction = $this->getSourceTransaction($journal);
$destTransaction = $this->getDestinationTransaction($journal);
$sourceAccount = $sourceTransaction->account;
$sourceAccountType = $sourceAccount->accountType->type;
$destAccount = $destTransaction->account;
$destAccountType = $destAccount->accountType->type;
if (!array_key_exists($type, $this->expected)) {
Log::info(sprintf('No source/destination info for transaction type %s.', $type));
$this->friendlyError(sprintf('No source/destination info for transaction type %s.', $type));
return;
}
if (!array_key_exists($sourceAccountType, $this->expected[$type])) {
Log::debug(sprintf('Going to fix journal #%d', $journal->id));
$this->fixJournal($journal, $type, $sourceTransaction, $destTransaction);
return;
}
$expectedTypes = $this->expected[$type][$sourceAccountType];
if (!in_array($destAccountType, $expectedTypes, true)) {
Log::debug(sprintf('Going to fix journal #%d', $journal->id));
$this->fixJournal($journal, $type, $sourceTransaction, $destTransaction);
}
}
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*
*/
private function stupidLaravel(): void
{
$this->count = 0;
}
} }

View File

@@ -58,6 +58,28 @@ class FixIbans extends Command
return 0; return 0;
} }
/**
* @param Collection $accounts
*
* @return void
*/
private function filterIbans(Collection $accounts): void
{
/** @var Account $account */
foreach ($accounts as $account) {
$iban = $account->iban;
if (str_contains($iban, ' ')) {
$iban = app('steam')->filterSpaces((string)$account->iban);
if ('' !== $iban) {
$account->iban = $iban;
$account->save();
$this->friendlyInfo(sprintf('Removed spaces from IBAN of account #%d', $account->id));
$this->count++;
}
}
}
}
/** /**
* @param Collection $accounts * @param Collection $accounts
* *
@@ -105,26 +127,4 @@ class FixIbans extends Command
} }
} }
} }
/**
* @param Collection $accounts
*
* @return void
*/
private function filterIbans(Collection $accounts): void
{
/** @var Account $account */
foreach ($accounts as $account) {
$iban = $account->iban;
if (str_contains($iban, ' ')) {
$iban = app('steam')->filterSpaces((string)$account->iban);
if ('' !== $iban) {
$account->iban = $iban;
$account->save();
$this->friendlyInfo(sprintf('Removed spaces from IBAN of account #%d', $account->id));
$this->count++;
}
}
}
}
} }

View File

@@ -62,6 +62,19 @@ class FixRecurringTransactions extends Command
return 0; return 0;
} }
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*
*/
private function stupidLaravel(): void
{
$this->recurringRepos = app(RecurringRepositoryInterface::class);
$this->userRepos = app(UserRepositoryInterface::class);
}
/** /**
* *
*/ */
@@ -74,6 +87,19 @@ class FixRecurringTransactions extends Command
} }
} }
/**
* @param User $user
*/
private function processUser(User $user): void
{
$this->recurringRepos->setUser($user);
$recurrences = $this->recurringRepos->get();
/** @var Recurrence $recurrence */
foreach ($recurrences as $recurrence) {
$this->processRecurrence($recurrence);
}
}
/** /**
* @param Recurrence $recurrence * @param Recurrence $recurrence
*/ */
@@ -107,30 +133,4 @@ class FixRecurringTransactions extends Command
} }
} }
} }
/**
* @param User $user
*/
private function processUser(User $user): void
{
$this->recurringRepos->setUser($user);
$recurrences = $this->recurringRepos->get();
/** @var Recurrence $recurrence */
foreach ($recurrences as $recurrence) {
$this->processRecurrence($recurrence);
}
}
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*
*/
private function stupidLaravel(): void
{
$this->recurringRepos = app(RecurringRepositoryInterface::class);
$this->userRepos = app(UserRepositoryInterface::class);
}
} }

View File

@@ -69,19 +69,6 @@ class FixTransactionTypes extends Command
return 0; return 0;
} }
/**
* @param TransactionJournal $journal
* @param string $expectedType
*/
private function changeJournal(TransactionJournal $journal, string $expectedType): void
{
$type = TransactionType::whereType($expectedType)->first();
if (null !== $type) {
$journal->transaction_type_id = $type->id;
$journal->save();
}
}
/** /**
* Collect all transaction journals. * Collect all transaction journals.
* *
@@ -129,6 +116,36 @@ class FixTransactionTypes extends Command
return false; return false;
} }
/**
* @param TransactionJournal $journal
*
* @return Account
* @throws FireflyException
*/
private function getSourceAccount(TransactionJournal $journal): Account
{
$collection = $journal->transactions->filter(
static function (Transaction $transaction) {
return $transaction->amount < 0;
}
);
if (0 === $collection->count()) {
throw new FireflyException(sprintf('300001: Journal #%d has no source transaction.', $journal->id));
}
if (1 !== $collection->count()) {
throw new FireflyException(sprintf('300002: Journal #%d has multiple source transactions.', $journal->id));
}
/** @var Transaction $transaction */
$transaction = $collection->first();
/** @var Account|null $account */
$account = $transaction->account;
if (null === $account) {
throw new FireflyException(sprintf('300003: Journal #%d, transaction #%d has no source account.', $journal->id, $transaction->id));
}
return $account;
}
/** /**
* @param TransactionJournal $journal * @param TransactionJournal $journal
* *
@@ -161,31 +178,14 @@ class FixTransactionTypes extends Command
/** /**
* @param TransactionJournal $journal * @param TransactionJournal $journal
* * @param string $expectedType
* @return Account
* @throws FireflyException
*/ */
private function getSourceAccount(TransactionJournal $journal): Account private function changeJournal(TransactionJournal $journal, string $expectedType): void
{ {
$collection = $journal->transactions->filter( $type = TransactionType::whereType($expectedType)->first();
static function (Transaction $transaction) { if (null !== $type) {
return $transaction->amount < 0; $journal->transaction_type_id = $type->id;
} $journal->save();
); }
if (0 === $collection->count()) {
throw new FireflyException(sprintf('300001: Journal #%d has no source transaction.', $journal->id));
}
if (1 !== $collection->count()) {
throw new FireflyException(sprintf('300002: Journal #%d has multiple source transactions.', $journal->id));
}
/** @var Transaction $transaction */
$transaction = $collection->first();
/** @var Account|null $account */
$account = $transaction->account;
if (null === $account) {
throw new FireflyException(sprintf('300003: Journal #%d, transaction #%d has no source account.', $journal->id, $transaction->id));
}
return $account;
} }
} }

View File

@@ -7,7 +7,6 @@ namespace FireflyIII\Console\Commands\Correction;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Services\Internal\Support\CreditRecalculateService; use FireflyIII\Services\Internal\Support\CreditRecalculateService;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
/** /**
* Class CorrectionSkeleton * Class CorrectionSkeleton
@@ -30,6 +29,16 @@ class TriggerCreditCalculation extends Command
return 0; return 0;
} }
private function processAccounts(): void
{
$accounts = Account::leftJoin('account_types', 'accounts.account_type_id', 'account_types.id')
->whereIn('account_types.type', config('firefly.valid_liabilities'))
->get(['accounts.*']);
foreach ($accounts as $account) {
$this->processAccount($account);
}
}
/** /**
* @param Account $account * @param Account $account
* *
@@ -42,14 +51,4 @@ class TriggerCreditCalculation extends Command
$object->setAccount($account); $object->setAccount($account);
$object->recalculate(); $object->recalculate();
} }
private function processAccounts(): void
{
$accounts = Account::leftJoin('account_types', 'accounts.account_type_id', 'account_types.id')
->whereIn('account_types.type', config('firefly.valid_liabilities'))
->get(['accounts.*']);
foreach ($accounts as $account) {
$this->processAccount($account);
}
}
} }

View File

@@ -36,8 +36,8 @@ use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Support\Export\ExportDataGenerator; use FireflyIII\Support\Export\ExportDataGenerator;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use InvalidArgumentException;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use InvalidArgumentException;
/** /**
* Class ExportData * Class ExportData
@@ -141,57 +141,48 @@ class ExportData extends Command
} }
/** /**
* @param array $options * Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* @param array $data * executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
* *
* @throws FireflyException
*/ */
private function exportData(array $options, array $data): void private function stupidLaravel(): void
{ {
$date = date('Y_m_d'); $this->journalRepository = app(JournalRepositoryInterface::class);
foreach ($data as $key => $content) { $this->accountRepository = app(AccountRepositoryInterface::class);
$file = sprintf('%s%s_%s.csv', $options['directory'], $date, $key);
if (false === $options['force'] && file_exists($file)) {
throw new FireflyException(sprintf('File "%s" exists already. Use --force to overwrite.', $file));
}
if (true === $options['force'] && file_exists($file)) {
$this->friendlyWarning(sprintf('File "%s" exists already but will be replaced.', $file));
}
// continue to write to file.
file_put_contents($file, $content);
$this->friendlyPositive(sprintf('Wrote %s-export to file "%s".', $key, $file));
}
} }
/** /**
* @return Collection * @return array
* @throws FireflyException * @throws FireflyException
* @throws Exception
*/ */
private function getAccountsParameter(): Collection private function parseOptions(): array
{ {
$final = new Collection(); $start = $this->getDateParameter('start');
$accounts = new Collection(); $end = $this->getDateParameter('end');
$accountList = $this->option('accounts'); $accounts = $this->getAccountsParameter();
$types = [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE]; $export = $this->getExportDirectory();
if (null !== $accountList && '' !== (string)$accountList) {
$accountIds = explode(',', $accountList);
$accounts = $this->accountRepository->getAccountsById($accountIds);
}
if (null === $accountList) {
$accounts = $this->accountRepository->getAccountsByType($types);
}
// filter accounts,
/** @var Account $account */
foreach ($accounts as $account) {
if (in_array($account->accountType->type, $types, true)) {
$final->push($account);
}
}
if (0 === $final->count()) {
throw new FireflyException('300007: Ended up with zero valid accounts to export from.');
}
return $final; return [
'export' => [
'transactions' => $this->option('export-transactions'),
'accounts' => $this->option('export-accounts'),
'budgets' => $this->option('export-budgets'),
'categories' => $this->option('export-categories'),
'tags' => $this->option('export-tags'),
'recurring' => $this->option('export-recurring'),
'rules' => $this->option('export-rules'),
'bills' => $this->option('export-bills'),
'piggies' => $this->option('export-piggies'),
],
'start' => $start,
'end' => $end,
'accounts' => $accounts,
'directory' => $export,
'force' => $this->option('force'),
];
} }
/** /**
@@ -235,6 +226,37 @@ class ExportData extends Command
return $date; return $date;
} }
/**
* @return Collection
* @throws FireflyException
*/
private function getAccountsParameter(): Collection
{
$final = new Collection();
$accounts = new Collection();
$accountList = $this->option('accounts');
$types = [AccountType::ASSET, AccountType::LOAN, AccountType::DEBT, AccountType::MORTGAGE];
if (null !== $accountList && '' !== (string)$accountList) {
$accountIds = explode(',', $accountList);
$accounts = $this->accountRepository->getAccountsById($accountIds);
}
if (null === $accountList) {
$accounts = $this->accountRepository->getAccountsByType($types);
}
// filter accounts,
/** @var Account $account */
foreach ($accounts as $account) {
if (in_array($account->accountType->type, $types, true)) {
$final->push($account);
}
}
if (0 === $final->count()) {
throw new FireflyException('300007: Ended up with zero valid accounts to export from.');
}
return $final;
}
/** /**
* @return string * @return string
* @throws FireflyException * @throws FireflyException
@@ -254,47 +276,25 @@ class ExportData extends Command
} }
/** /**
* @return array * @param array $options
* @throws FireflyException * @param array $data
* @throws Exception
*/
private function parseOptions(): array
{
$start = $this->getDateParameter('start');
$end = $this->getDateParameter('end');
$accounts = $this->getAccountsParameter();
$export = $this->getExportDirectory();
return [
'export' => [
'transactions' => $this->option('export-transactions'),
'accounts' => $this->option('export-accounts'),
'budgets' => $this->option('export-budgets'),
'categories' => $this->option('export-categories'),
'tags' => $this->option('export-tags'),
'recurring' => $this->option('export-recurring'),
'rules' => $this->option('export-rules'),
'bills' => $this->option('export-bills'),
'piggies' => $this->option('export-piggies'),
],
'start' => $start,
'end' => $end,
'accounts' => $accounts,
'directory' => $export,
'force' => $this->option('force'),
];
}
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
* *
* @throws FireflyException
*/ */
private function stupidLaravel(): void private function exportData(array $options, array $data): void
{ {
$this->journalRepository = app(JournalRepositoryInterface::class); $date = date('Y_m_d');
$this->accountRepository = app(AccountRepositoryInterface::class); foreach ($data as $key => $content) {
$file = sprintf('%s%s_%s.csv', $options['directory'], $date, $key);
if (false === $options['force'] && file_exists($file)) {
throw new FireflyException(sprintf('File "%s" exists already. Use --force to overwrite.', $file));
}
if (true === $options['force'] && file_exists($file)) {
$this->friendlyWarning(sprintf('File "%s" exists already but will be replaced.', $file));
}
// continue to write to file.
file_put_contents($file, $content);
$this->friendlyPositive(sprintf('Wrote %s-export to file "%s".', $key, $file));
}
} }
} }

View File

@@ -31,7 +31,6 @@ use FireflyIII\Models\UserGroup;
use FireflyIII\Models\UserRole; use FireflyIII\Models\UserRole;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
/** /**
* Class CreateGroupMemberships * Class CreateGroupMemberships
@@ -44,6 +43,33 @@ class CreateGroupMemberships extends Command
protected $description = 'Update group memberships'; protected $description = 'Update group memberships';
protected $signature = 'firefly-iii:create-group-memberships'; protected $signature = 'firefly-iii:create-group-memberships';
/**
* Execute the console command.
*
* @return int
* @throws FireflyException
*/
public function handle(): int
{
$this->createGroupMemberships();
$this->friendlyPositive('Validated group memberships');
return 0;
}
/**
*
* @throws FireflyException
*/
private function createGroupMemberships(): void
{
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
self::createGroupMembership($user);
}
}
/** /**
* TODO move to helper. * TODO move to helper.
* *
@@ -81,31 +107,4 @@ class CreateGroupMemberships extends Command
$user->save(); $user->save();
} }
} }
/**
* Execute the console command.
*
* @return int
* @throws FireflyException
*/
public function handle(): int
{
$this->createGroupMemberships();
$this->friendlyPositive('Validated group memberships');
return 0;
}
/**
*
* @throws FireflyException
*/
private function createGroupMemberships(): void
{
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
self::createGroupMembership($user);
}
}
} }

View File

@@ -67,51 +67,6 @@ class ReportEmptyObjects extends Command
return 0; return 0;
} }
/**
* Reports on accounts with no transactions.
*/
private function reportAccounts(): void
{
$set = Account::leftJoin('transactions', 'transactions.account_id', '=', 'accounts.id')
->leftJoin('users', 'accounts.user_id', '=', 'users.id')
->groupBy(['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email'])
->whereNull('transactions.account_id')
->get(
['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email']
);
/** @var stdClass $entry */
foreach ($set as $entry) {
$line = 'User #%d (%s) has account #%d ("%s") which has no transactions.';
$line = sprintf($line, $entry->user_id, $entry->email, $entry->id, $entry->name);
$this->friendlyWarning($line);
}
}
/**
* Reports on budgets with no budget limits (which makes them pointless).
*/
private function reportBudgetLimits(): void
{
$set = Budget::leftJoin('budget_limits', 'budget_limits.budget_id', '=', 'budgets.id')
->leftJoin('users', 'budgets.user_id', '=', 'users.id')
->groupBy(['budgets.id', 'budgets.name', 'budgets.encrypted', 'budgets.user_id', 'users.email'])
->whereNull('budget_limits.id')
->get(['budgets.id', 'budgets.name', 'budgets.user_id', 'budgets.encrypted', 'users.email']);
/** @var Budget $entry */
foreach ($set as $entry) {
$line = sprintf(
'User #%d (%s) has budget #%d ("%s") which has no budget limits.',
$entry->user_id,
$entry->email,
$entry->id,
$entry->name
);
$this->friendlyWarning($line);
}
}
/** /**
* Report on budgets with no transactions or journals. * Report on budgets with no transactions or journals.
*/ */
@@ -186,4 +141,49 @@ class ReportEmptyObjects extends Command
$this->friendlyWarning($line); $this->friendlyWarning($line);
} }
} }
/**
* Reports on accounts with no transactions.
*/
private function reportAccounts(): void
{
$set = Account::leftJoin('transactions', 'transactions.account_id', '=', 'accounts.id')
->leftJoin('users', 'accounts.user_id', '=', 'users.id')
->groupBy(['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email'])
->whereNull('transactions.account_id')
->get(
['accounts.id', 'accounts.encrypted', 'accounts.name', 'accounts.user_id', 'users.email']
);
/** @var stdClass $entry */
foreach ($set as $entry) {
$line = 'User #%d (%s) has account #%d ("%s") which has no transactions.';
$line = sprintf($line, $entry->user_id, $entry->email, $entry->id, $entry->name);
$this->friendlyWarning($line);
}
}
/**
* Reports on budgets with no budget limits (which makes them pointless).
*/
private function reportBudgetLimits(): void
{
$set = Budget::leftJoin('budget_limits', 'budget_limits.budget_id', '=', 'budgets.id')
->leftJoin('users', 'budgets.user_id', '=', 'users.id')
->groupBy(['budgets.id', 'budgets.name', 'budgets.encrypted', 'budgets.user_id', 'users.email'])
->whereNull('budget_limits.id')
->get(['budgets.id', 'budgets.name', 'budgets.user_id', 'budgets.encrypted', 'users.email']);
/** @var Budget $entry */
foreach ($set as $entry) {
$line = sprintf(
'User #%d (%s) has budget #%d ("%s") which has no budget limits.',
$entry->user_id,
$entry->email,
$entry->id,
$entry->name
);
$this->friendlyWarning($line);
}
}
} }

View File

@@ -50,38 +50,6 @@ class RestoreOAuthKeys extends Command
return 0; return 0;
} }
/**
*
*/
private function generateKeys(): void
{
OAuthKeys::generateKeys();
}
/**
* @return bool
*/
private function keysInDatabase(): bool
{
return OAuthKeys::keysInDatabase();
}
/**
* @return bool
*/
private function keysOnDrive(): bool
{
return OAuthKeys::hasKeyFiles();
}
/**
*
*/
private function restoreKeysFromDB(): bool
{
return OAuthKeys::restoreKeysFromDB();
}
/** /**
* *
*/ */
@@ -116,6 +84,30 @@ class RestoreOAuthKeys extends Command
$this->friendlyPositive('OAuth keys are OK'); $this->friendlyPositive('OAuth keys are OK');
} }
/**
* @return bool
*/
private function keysInDatabase(): bool
{
return OAuthKeys::keysInDatabase();
}
/**
* @return bool
*/
private function keysOnDrive(): bool
{
return OAuthKeys::hasKeyFiles();
}
/**
*
*/
private function generateKeys(): void
{
OAuthKeys::generateKeys();
}
/** /**
* *
*/ */
@@ -123,4 +115,12 @@ class RestoreOAuthKeys extends Command
{ {
OAuthKeys::storeKeysInDB(); OAuthKeys::storeKeysInDB();
} }
/**
*
*/
private function restoreKeysFromDB(): bool
{
return OAuthKeys::restoreKeysFromDB();
}
} }

View File

@@ -50,18 +50,18 @@ trait ShowsFriendlyMessages
* @param string $message * @param string $message
* @return void * @return void
*/ */
public function friendlyLine(string $message): void public function friendlyNeutral(string $message): void
{ {
$this->line(sprintf(' %s', trim($message))); $this->line(sprintf(' [i] %s', trim($message)));
} }
/** /**
* @param string $message * @param string $message
* @return void * @return void
*/ */
public function friendlyNeutral(string $message): void public function friendlyLine(string $message): void
{ {
$this->line(sprintf(' [i] %s', trim($message))); $this->line(sprintf(' %s', trim($message)));
} }
/** /**

View File

@@ -110,52 +110,19 @@ class ForceDecimalSize extends Command
return 0; return 0;
} }
/** private function determineDatabaseType(): void
* This method loops over all accounts and validates the amounts.
*
* @param TransactionCurrency $currency
* @param array $fields
*
* @return void
*/
private function correctAccountAmounts(TransactionCurrency $currency, array $fields): void
{ {
$operator = $this->operator; // switch stuff based on database connection:
$cast = $this->cast; $this->operator = 'REGEXP';
$regularExpression = $this->regularExpression; $this->regularExpression = '\'\\\\.[\\\\d]{%d}[1-9]+\'';
$this->cast = 'CHAR';
/** @var Builder $query */ if ('pgsql' === config('database.default')) {
$query = Account::leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id') $this->operator = 'SIMILAR TO';
->where('account_meta.name', 'currency_id') $this->regularExpression = '\'%%\.[\d]{%d}[1-9]+%%\'';
->where('account_meta.data', json_encode((string)$currency->id)); $this->cast = 'TEXT';
$query->where(static function (Builder $q) use ($fields, $currency, $operator, $cast, $regularExpression) {
foreach ($fields as $field) {
$q->orWhere(
DB::raw(sprintf('CAST(accounts.%s AS %s)', $field, $cast)),
$operator,
DB::raw(sprintf($regularExpression, $currency->decimal_places))
);
}
});
$result = $query->get(['accounts.*']);
if (0 === $result->count()) {
$this->friendlyPositive(sprintf('All accounts in %s are OK', $currency->code));
return;
}
/** @var Account $account */
foreach ($result as $account) {
foreach ($fields as $field) {
$value = $account->$field;
if (null === $value) {
continue;
}
// fix $field by rounding it down correctly.
$pow = pow(10, (int)$currency->decimal_places);
$correct = bcdiv((string)round($value * $pow), (string)$pow, 12);
$this->friendlyInfo(sprintf('Account #%d has %s with value "%s", this has been corrected to "%s".', $account->id, $field, $value, $correct));
Account::find($account->id)->update([$field => $correct]);
} }
if ('sqlite' === config('database.default')) {
$this->regularExpression = '"\\.[\d]{%d}[1-9]+"';
} }
} }
@@ -249,6 +216,55 @@ class ForceDecimalSize extends Command
} }
} }
/**
* This method loops over all accounts and validates the amounts.
*
* @param TransactionCurrency $currency
* @param array $fields
*
* @return void
*/
private function correctAccountAmounts(TransactionCurrency $currency, array $fields): void
{
$operator = $this->operator;
$cast = $this->cast;
$regularExpression = $this->regularExpression;
/** @var Builder $query */
$query = Account::leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id')
->where('account_meta.name', 'currency_id')
->where('account_meta.data', json_encode((string)$currency->id));
$query->where(static function (Builder $q) use ($fields, $currency, $operator, $cast, $regularExpression) {
foreach ($fields as $field) {
$q->orWhere(
DB::raw(sprintf('CAST(accounts.%s AS %s)', $field, $cast)),
$operator,
DB::raw(sprintf($regularExpression, $currency->decimal_places))
);
}
});
$result = $query->get(['accounts.*']);
if (0 === $result->count()) {
$this->friendlyPositive(sprintf('All accounts in %s are OK', $currency->code));
return;
}
/** @var Account $account */
foreach ($result as $account) {
foreach ($fields as $field) {
$value = $account->$field;
if (null === $value) {
continue;
}
// fix $field by rounding it down correctly.
$pow = pow(10, (int)$currency->decimal_places);
$correct = bcdiv((string)round($value * $pow), (string)$pow, 12);
$this->friendlyInfo(sprintf('Account #%d has %s with value "%s", this has been corrected to "%s".', $account->id, $field, $value, $correct));
Account::find($account->id)->update([$field => $correct]);
}
}
}
/** /**
* This method fixes all auto budgets in currency $currency. * This method fixes all auto budgets in currency $currency.
* *
@@ -300,57 +316,6 @@ class ForceDecimalSize extends Command
} }
} }
/**
* This method fixes all piggy banks in currency $currency.
*
* @param TransactionCurrency $currency
* @param array $fields
*
* @return void
*/
private function correctPiggyAmounts(TransactionCurrency $currency, array $fields): void
{
$operator = $this->operator;
$cast = $this->cast;
$regularExpression = $this->regularExpression;
/** @var Builder $query */
$query = PiggyBank::leftJoin('accounts', 'piggy_banks.account_id', '=', 'accounts.id')
->leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id')
->where('account_meta.name', 'currency_id')
->where('account_meta.data', json_encode((string)$currency->id))
->where(static function (Builder $q) use ($fields, $currency, $operator, $cast, $regularExpression) {
foreach ($fields as $field) {
$q->orWhere(
DB::raw(sprintf('CAST(piggy_banks.%s AS %s)', $field, $cast)),
$operator,
DB::raw(sprintf($regularExpression, $currency->decimal_places))
);
}
});
$result = $query->get(['piggy_banks.*']);
if (0 === $result->count()) {
$this->friendlyPositive(sprintf('All piggy banks in %s are OK', $currency->code));
return;
}
/** @var PiggyBank $item */
foreach ($result as $item) {
foreach ($fields as $field) {
$value = $item->$field;
if (null === $value) {
continue;
}
// fix $field by rounding it down correctly.
$pow = pow(10, (int)$currency->decimal_places);
$correct = bcdiv((string)round($value * $pow), (string)$pow, 12);
$this->friendlyWarning(sprintf('Piggy bank #%d has %s with value "%s", this has been corrected to "%s".', $item->id, $field, $value, $correct));
PiggyBank::find($item->id)->update([$field => $correct]);
}
}
}
/** /**
* This method fixes all piggy bank events in currency $currency. * This method fixes all piggy bank events in currency $currency.
* *
@@ -459,6 +424,57 @@ class ForceDecimalSize extends Command
} }
} }
/**
* This method fixes all piggy banks in currency $currency.
*
* @param TransactionCurrency $currency
* @param array $fields
*
* @return void
*/
private function correctPiggyAmounts(TransactionCurrency $currency, array $fields): void
{
$operator = $this->operator;
$cast = $this->cast;
$regularExpression = $this->regularExpression;
/** @var Builder $query */
$query = PiggyBank::leftJoin('accounts', 'piggy_banks.account_id', '=', 'accounts.id')
->leftJoin('account_meta', 'accounts.id', '=', 'account_meta.account_id')
->where('account_meta.name', 'currency_id')
->where('account_meta.data', json_encode((string)$currency->id))
->where(static function (Builder $q) use ($fields, $currency, $operator, $cast, $regularExpression) {
foreach ($fields as $field) {
$q->orWhere(
DB::raw(sprintf('CAST(piggy_banks.%s AS %s)', $field, $cast)),
$operator,
DB::raw(sprintf($regularExpression, $currency->decimal_places))
);
}
});
$result = $query->get(['piggy_banks.*']);
if (0 === $result->count()) {
$this->friendlyPositive(sprintf('All piggy banks in %s are OK', $currency->code));
return;
}
/** @var PiggyBank $item */
foreach ($result as $item) {
foreach ($fields as $field) {
$value = $item->$field;
if (null === $value) {
continue;
}
// fix $field by rounding it down correctly.
$pow = pow(10, (int)$currency->decimal_places);
$correct = bcdiv((string)round($value * $pow), (string)$pow, 12);
$this->friendlyWarning(sprintf('Piggy bank #%d has %s with value "%s", this has been corrected to "%s".', $item->id, $field, $value, $correct));
PiggyBank::find($item->id)->update([$field => $correct]);
}
}
}
/** /**
* This method fixes all transactions in currency $currency. * This method fixes all transactions in currency $currency.
* *
@@ -524,22 +540,6 @@ class ForceDecimalSize extends Command
} }
} }
private function determineDatabaseType(): void
{
// switch stuff based on database connection:
$this->operator = 'REGEXP';
$this->regularExpression = '\'\\\\.[\\\\d]{%d}[1-9]+\'';
$this->cast = 'CHAR';
if ('pgsql' === config('database.default')) {
$this->operator = 'SIMILAR TO';
$this->regularExpression = '\'%%\.[\d]{%d}[1-9]+%%\'';
$this->cast = 'TEXT';
}
if ('sqlite' === config('database.default')) {
$this->regularExpression = '"\\.[\d]{%d}[1-9]+"';
}
}
/** /**
* @return void * @return void
*/ */

View File

@@ -66,6 +66,93 @@ class UpgradeFireflyInstructions extends Command
return 0; return 0;
} }
/**
* Render upgrade instructions.
*/
private function updateInstructions(): void
{
/** @var string $version */
$version = config('firefly.version');
$config = config('upgrade.text.upgrade');
$text = '';
foreach (array_keys($config) as $compare) {
// if string starts with:
if (\str_starts_with($version, $compare)) {
$text = $config[$compare];
}
}
$this->newLine();
$this->showLogo();
$this->newLine();
$this->showLine();
$this->boxed('');
if (null === $text || '' === $text) {
$this->boxed(sprintf('Thank you for updating to Firefly III, v%s', $version));
$this->boxedInfo('There are no extra upgrade instructions.');
$this->boxed('Firefly III should be ready for use.');
$this->boxed('');
$this->showLine();
return;
}
$this->boxed(sprintf('Thank you for updating to Firefly III, v%s!', $version));
$this->boxedInfo($text);
$this->boxed('');
$this->showLine();
}
/**
* The logo takes up 8 lines of code. So 8 colors can be used.
*
* @return void
*/
private function showLogo(): void
{
$today = date('m-d');
$month = date('m');
// variation in colors and effects just because I can!
// default is Ukraine flag:
$colors = ['blue', 'blue', 'blue', 'yellow', 'yellow', 'yellow', 'default', 'default'];
// 5th of May is Dutch liberation day and 29th of April is Dutch King's Day and September 17 is my birthday.
if ('05-01' === $today || '04-29' === $today || '09-17' === $today) {
$colors = ['red', 'red', 'red', 'white', 'white', 'blue', 'blue', 'blue'];
}
// National Coming Out Day, International Day Against Homophobia, Biphobia and Transphobia and Pride Month
if ('10-11' === $today || '05-17' === $today || '06' === $month) {
$colors = ['red', 'bright-red', 'yellow', 'green', 'blue', 'magenta', 'default', 'default'];
}
// International Transgender Day of Visibility
if ('03-31' === $today) {
$colors = ['bright-blue', 'bright-red', 'white', 'white', 'bright-red', 'bright-blue', 'default', 'default'];
}
$this->line(sprintf('<fg=%s> ______ _ __ _ _____ _____ _____ </>', $colors[0]));
$this->line(sprintf('<fg=%s> | ____(_) / _| | |_ _|_ _|_ _| </>', $colors[1]));
$this->line(sprintf('<fg=%s> | |__ _ _ __ ___| |_| |_ _ | | | | | | </>', $colors[2]));
$this->line(sprintf('<fg=%s> | __| | | \'__/ _ \ _| | | | | | | | | | | </>', $colors[3]));
$this->line(sprintf('<fg=%s> | | | | | | __/ | | | |_| | _| |_ _| |_ _| |_ </>', $colors[4]));
$this->line(sprintf('<fg=%s> |_| |_|_| \___|_| |_|\__, | |_____|_____|_____| </>', $colors[5]));
$this->line(sprintf('<fg=%s> __/ | </>', $colors[6]));
$this->line(sprintf('<fg=%s> |___/ </>', $colors[7]));
}
/**
* Show a line.
*/
private function showLine(): void
{
$line = '+';
$line .= str_repeat('-', 78);
$line .= '+';
$this->line($line);
}
/** /**
* Show a nice box. * Show a nice box.
* *
@@ -127,90 +214,4 @@ class UpgradeFireflyInstructions extends Command
$this->boxed(''); $this->boxed('');
$this->showLine(); $this->showLine();
} }
/**
* Show a line.
*/
private function showLine(): void
{
$line = '+';
$line .= str_repeat('-', 78);
$line .= '+';
$this->line($line);
}
/**
* The logo takes up 8 lines of code. So 8 colors can be used.
* @return void
*/
private function showLogo(): void
{
$today = date('m-d');
$month = date('m');
// variation in colors and effects just because I can!
// default is Ukraine flag:
$colors = ['blue', 'blue', 'blue', 'yellow', 'yellow', 'yellow', 'default', 'default'];
// 5th of May is Dutch liberation day and 29th of April is Dutch King's Day and September 17 is my birthday.
if ('05-01' === $today || '04-29' === $today || '09-17' === $today) {
$colors = ['red', 'red', 'red', 'white', 'white', 'blue', 'blue', 'blue'];
}
// National Coming Out Day, International Day Against Homophobia, Biphobia and Transphobia and Pride Month
if ('10-11' === $today || '05-17' === $today || '06' === $month) {
$colors = ['red', 'bright-red', 'yellow', 'green', 'blue', 'magenta', 'default', 'default'];
}
// International Transgender Day of Visibility
if ('03-31' === $today) {
$colors = ['bright-blue', 'bright-red', 'white', 'white', 'bright-red', 'bright-blue', 'default', 'default'];
}
$this->line(sprintf('<fg=%s> ______ _ __ _ _____ _____ _____ </>', $colors[0]));
$this->line(sprintf('<fg=%s> | ____(_) / _| | |_ _|_ _|_ _| </>', $colors[1]));
$this->line(sprintf('<fg=%s> | |__ _ _ __ ___| |_| |_ _ | | | | | | </>', $colors[2]));
$this->line(sprintf('<fg=%s> | __| | | \'__/ _ \ _| | | | | | | | | | | </>', $colors[3]));
$this->line(sprintf('<fg=%s> | | | | | | __/ | | | |_| | _| |_ _| |_ _| |_ </>', $colors[4]));
$this->line(sprintf('<fg=%s> |_| |_|_| \___|_| |_|\__, | |_____|_____|_____| </>', $colors[5]));
$this->line(sprintf('<fg=%s> __/ | </>', $colors[6]));
$this->line(sprintf('<fg=%s> |___/ </>', $colors[7]));
}
/**
* Render upgrade instructions.
*/
private function updateInstructions(): void
{
/** @var string $version */
$version = config('firefly.version');
$config = config('upgrade.text.upgrade');
$text = '';
foreach (array_keys($config) as $compare) {
// if string starts with:
if (\str_starts_with($version, $compare)) {
$text = $config[$compare];
}
}
$this->newLine();
$this->showLogo();
$this->newLine();
$this->showLine();
$this->boxed('');
if (null === $text || '' === $text) {
$this->boxed(sprintf('Thank you for updating to Firefly III, v%s', $version));
$this->boxedInfo('There are no extra upgrade instructions.');
$this->boxed('Firefly III should be ready for use.');
$this->boxed('');
$this->showLine();
return;
}
$this->boxed(sprintf('Thank you for updating to Firefly III, v%s!', $version));
$this->boxedInfo($text);
$this->boxed('');
$this->showLine();
}
} }

View File

@@ -153,49 +153,6 @@ class ApplyRules extends Command
return 0; return 0;
} }
/**
* @return Collection
*/
private function getRulesToApply(): Collection
{
$rulesToApply = new Collection();
/** @var RuleGroup $group */
foreach ($this->groups as $group) {
$rules = $this->ruleGroupRepository->getActiveStoreRules($group);
/** @var Rule $rule */
foreach ($rules as $rule) {
// if in rule selection, or group in selection or all rules, it's included.
$test = $this->includeRule($rule, $group);
if (true === $test) {
Log::debug(sprintf('Will include rule #%d "%s"', $rule->id, $rule->title));
$rulesToApply->push($rule);
}
}
}
return $rulesToApply;
}
/**
*/
private function grabAllRules(): void
{
$this->groups = $this->ruleGroupRepository->getActiveGroups();
}
/**
* @param Rule $rule
* @param RuleGroup $group
*
* @return bool
*/
private function includeRule(Rule $rule, RuleGroup $group): bool
{
return in_array($group->id, $this->ruleGroupSelection, true)
|| in_array($rule->id, $this->ruleSelection, true)
|| $this->allRules;
}
/** /**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is * Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should * executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
@@ -274,42 +231,6 @@ class ApplyRules extends Command
return true; return true;
} }
/**
* @throws FireflyException
*/
private function verifyInputDates(): void
{
// parse start date.
$inputStart = today(config('app.timezone'))->startOfMonth();
$startString = $this->option('start_date');
if (null === $startString) {
/** @var JournalRepositoryInterface $repository */
$repository = app(JournalRepositoryInterface::class);
$repository->setUser($this->getUser());
$first = $repository->firstNull();
if (null !== $first) {
$inputStart = $first->date;
}
}
if (null !== $startString && '' !== $startString) {
$inputStart = Carbon::createFromFormat('Y-m-d', $startString);
}
// parse end date
$inputEnd = today(config('app.timezone'));
$endString = $this->option('end_date');
if (null !== $endString && '' !== $endString) {
$inputEnd = Carbon::createFromFormat('Y-m-d', $endString);
}
if ($inputStart > $inputEnd) {
[$inputEnd, $inputStart] = [$inputStart, $inputEnd];
}
$this->startDate = $inputStart;
$this->endDate = $inputEnd;
}
/** /**
* @return bool * @return bool
*/ */
@@ -356,4 +277,83 @@ class ApplyRules extends Command
return true; return true;
} }
/**
* @throws FireflyException
*/
private function verifyInputDates(): void
{
// parse start date.
$inputStart = today(config('app.timezone'))->startOfMonth();
$startString = $this->option('start_date');
if (null === $startString) {
/** @var JournalRepositoryInterface $repository */
$repository = app(JournalRepositoryInterface::class);
$repository->setUser($this->getUser());
$first = $repository->firstNull();
if (null !== $first) {
$inputStart = $first->date;
}
}
if (null !== $startString && '' !== $startString) {
$inputStart = Carbon::createFromFormat('Y-m-d', $startString);
}
// parse end date
$inputEnd = today(config('app.timezone'));
$endString = $this->option('end_date');
if (null !== $endString && '' !== $endString) {
$inputEnd = Carbon::createFromFormat('Y-m-d', $endString);
}
if ($inputStart > $inputEnd) {
[$inputEnd, $inputStart] = [$inputStart, $inputEnd];
}
$this->startDate = $inputStart;
$this->endDate = $inputEnd;
}
/**
*/
private function grabAllRules(): void
{
$this->groups = $this->ruleGroupRepository->getActiveGroups();
}
/**
* @return Collection
*/
private function getRulesToApply(): Collection
{
$rulesToApply = new Collection();
/** @var RuleGroup $group */
foreach ($this->groups as $group) {
$rules = $this->ruleGroupRepository->getActiveStoreRules($group);
/** @var Rule $rule */
foreach ($rules as $rule) {
// if in rule selection, or group in selection or all rules, it's included.
$test = $this->includeRule($rule, $group);
if (true === $test) {
Log::debug(sprintf('Will include rule #%d "%s"', $rule->id, $rule->title));
$rulesToApply->push($rule);
}
}
}
return $rulesToApply;
}
/**
* @param Rule $rule
* @param RuleGroup $group
*
* @return bool
*/
private function includeRule(Rule $rule, RuleGroup $group): bool
{
return in_array($group->id, $this->ruleGroupSelection, true)
|| in_array($rule->id, $this->ruleSelection, true)
|| $this->allRules;
}
} }

View File

@@ -126,6 +126,62 @@ class Cron extends Command
return 0; return 0;
} }
/**
* @param bool $force
* @param Carbon|null $date
*/
private function exchangeRatesCronJob(bool $force, ?Carbon $date): void
{
$exchangeRates = new ExchangeRatesCronjob();
$exchangeRates->setForce($force);
// set date in cron job:
if (null !== $date) {
$exchangeRates->setDate($date);
}
$exchangeRates->fire();
if ($exchangeRates->jobErrored) {
$this->friendlyError(sprintf('Error in "exchange rates" cron: %s', $exchangeRates->message));
}
if ($exchangeRates->jobFired) {
$this->friendlyInfo(sprintf('"Exchange rates" cron fired: %s', $exchangeRates->message));
}
if ($exchangeRates->jobSucceeded) {
$this->friendlyPositive(sprintf('"Exchange rates" cron ran with success: %s', $exchangeRates->message));
}
}
/**
* @param bool $force
* @param Carbon|null $date
*
* @throws ContainerExceptionInterface
* @throws FireflyException
* @throws NotFoundExceptionInterface
*/
private function recurringCronJob(bool $force, ?Carbon $date): void
{
$recurring = new RecurringCronjob();
$recurring->setForce($force);
// set date in cron job:
if (null !== $date) {
$recurring->setDate($date);
}
$recurring->fire();
if ($recurring->jobErrored) {
$this->friendlyError(sprintf('Error in "create recurring transactions" cron: %s', $recurring->message));
}
if ($recurring->jobFired) {
$this->friendlyInfo(sprintf('"Create recurring transactions" cron fired: %s', $recurring->message));
}
if ($recurring->jobSucceeded) {
$this->friendlyPositive(sprintf('"Create recurring transactions" cron ran with success: %s', $recurring->message));
}
}
/** /**
* @param bool $force * @param bool $force
* @param Carbon|null $date * @param Carbon|null $date
@@ -182,60 +238,4 @@ class Cron extends Command
$this->friendlyPositive(sprintf('"Send bill warnings" cron ran with success: %s', $autoBudget->message)); $this->friendlyPositive(sprintf('"Send bill warnings" cron ran with success: %s', $autoBudget->message));
} }
} }
/**
* @param bool $force
* @param Carbon|null $date
*/
private function exchangeRatesCronJob(bool $force, ?Carbon $date): void
{
$exchangeRates = new ExchangeRatesCronjob();
$exchangeRates->setForce($force);
// set date in cron job:
if (null !== $date) {
$exchangeRates->setDate($date);
}
$exchangeRates->fire();
if ($exchangeRates->jobErrored) {
$this->friendlyError(sprintf('Error in "exchange rates" cron: %s', $exchangeRates->message));
}
if ($exchangeRates->jobFired) {
$this->friendlyInfo(sprintf('"Exchange rates" cron fired: %s', $exchangeRates->message));
}
if ($exchangeRates->jobSucceeded) {
$this->friendlyPositive(sprintf('"Exchange rates" cron ran with success: %s', $exchangeRates->message));
}
}
/**
* @param bool $force
* @param Carbon|null $date
*
* @throws ContainerExceptionInterface
* @throws FireflyException
* @throws NotFoundExceptionInterface
*/
private function recurringCronJob(bool $force, ?Carbon $date): void
{
$recurring = new RecurringCronjob();
$recurring->setForce($force);
// set date in cron job:
if (null !== $date) {
$recurring->setDate($date);
}
$recurring->fire();
if ($recurring->jobErrored) {
$this->friendlyError(sprintf('Error in "create recurring transactions" cron: %s', $recurring->message));
}
if ($recurring->jobFired) {
$this->friendlyInfo(sprintf('"Create recurring transactions" cron fired: %s', $recurring->message));
}
if ($recurring->jobSucceeded) {
$this->friendlyPositive(sprintf('"Create recurring transactions" cron ran with success: %s', $recurring->message));
}
}
} }

View File

@@ -35,8 +35,6 @@ use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
/** /**
* Class AccountCurrencies * Class AccountCurrencies
@@ -54,7 +52,8 @@ class AccountCurrencies extends Command
private UserRepositoryInterface $userRepos; private UserRepositoryInterface $userRepos;
/** /**
* Each (asset) account must have a reference to a preferred currency. If the account does not have one, it's forced upon the account. * Each (asset) account must have a reference to a preferred currency. If the account does not have one, it's
* forced upon the account.
* *
* @return int * @return int
*/ */
@@ -80,6 +79,19 @@ class AccountCurrencies extends Command
return 0; return 0;
} }
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*
*/
private function stupidLaravel(): void
{
$this->accountRepos = app(AccountRepositoryInterface::class);
$this->userRepos = app(UserRepositoryInterface::class);
$this->count = 0;
}
/** /**
* @return bool * @return bool
*/ */
@@ -92,22 +104,46 @@ class AccountCurrencies extends Command
/** /**
* *
*/ */
private function markAsExecuted(): void private function updateAccountCurrencies(): void
{ {
app('fireflyconfig')->set(self::CONFIG_NAME, true); $users = $this->userRepos->all();
$defaultCurrencyCode = (string)config('firefly.default_currency', 'EUR');
foreach ($users as $user) {
$this->updateCurrenciesForUser($user, $defaultCurrencyCode);
}
} }
/** /**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is * @param User $user
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should * @param string $systemCurrencyCode
* be called from the handle method instead of using the constructor to initialize the command.
* *
* @throws FireflyException
*/ */
private function stupidLaravel(): void private function updateCurrenciesForUser(User $user, string $systemCurrencyCode): void
{ {
$this->accountRepos = app(AccountRepositoryInterface::class); $this->accountRepos->setUser($user);
$this->userRepos = app(UserRepositoryInterface::class); $accounts = $this->accountRepos->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]);
$this->count = 0;
// get user's currency preference:
$defaultCurrencyCode = app('preferences')->getForUser($user, 'currencyPreference', $systemCurrencyCode)->data;
if (!is_string($defaultCurrencyCode)) {
$defaultCurrencyCode = $systemCurrencyCode;
}
/** @var TransactionCurrency|null $defaultCurrency */
$defaultCurrency = TransactionCurrency::where('code', $defaultCurrencyCode)->first();
if (null === $defaultCurrency) {
Log::error(sprintf('Users currency pref "%s" does not exist!', $defaultCurrencyCode));
$this->friendlyError(sprintf('User has a preference for "%s", but this currency does not exist.', $defaultCurrencyCode));
return;
}
/** @var Account $account */
foreach ($accounts as $account) {
$this->updateAccount($account, $defaultCurrency);
}
} }
/** /**
@@ -158,45 +194,8 @@ class AccountCurrencies extends Command
/** /**
* *
*/ */
private function updateAccountCurrencies(): void private function markAsExecuted(): void
{ {
$users = $this->userRepos->all(); app('fireflyconfig')->set(self::CONFIG_NAME, true);
$defaultCurrencyCode = (string)config('firefly.default_currency', 'EUR');
foreach ($users as $user) {
$this->updateCurrenciesForUser($user, $defaultCurrencyCode);
}
}
/**
* @param User $user
* @param string $systemCurrencyCode
*
* @throws FireflyException
*/
private function updateCurrenciesForUser(User $user, string $systemCurrencyCode): void
{
$this->accountRepos->setUser($user);
$accounts = $this->accountRepos->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]);
// get user's currency preference:
$defaultCurrencyCode = app('preferences')->getForUser($user, 'currencyPreference', $systemCurrencyCode)->data;
if (!is_string($defaultCurrencyCode)) {
$defaultCurrencyCode = $systemCurrencyCode;
}
/** @var TransactionCurrency|null $defaultCurrency */
$defaultCurrency = TransactionCurrency::where('code', $defaultCurrencyCode)->first();
if (null === $defaultCurrency) {
Log::error(sprintf('Users currency pref "%s" does not exist!', $defaultCurrencyCode));
$this->friendlyError(sprintf('User has a preference for "%s", but this currency does not exist.', $defaultCurrencyCode));
return;
}
/** @var Account $account */
foreach ($accounts as $account) {
$this->updateAccount($account, $defaultCurrency);
}
} }
} }

View File

@@ -71,6 +71,30 @@ class AppendBudgetLimitPeriods extends Command
return 0; return 0;
} }
/**
* @return bool
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function isExecuted(): bool
{
$configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false);
return (bool)$configVar->data;
}
/**
*
*/
private function theresNoLimit(): void
{
$limits = BudgetLimit::whereNull('period')->get();
/** @var BudgetLimit $limit */
foreach ($limits as $limit) {
$this->fixLimit($limit);
}
}
/** /**
* @param BudgetLimit $limit * @param BudgetLimit $limit
*/ */
@@ -156,18 +180,6 @@ class AppendBudgetLimitPeriods extends Command
return null; return null;
} }
/**
* @return bool
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function isExecuted(): bool
{
$configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false);
return (bool)$configVar->data;
}
/** /**
* *
*/ */
@@ -175,16 +187,4 @@ class AppendBudgetLimitPeriods extends Command
{ {
app('fireflyconfig')->set(self::CONFIG_NAME, true); app('fireflyconfig')->set(self::CONFIG_NAME, true);
} }
/**
*
*/
private function theresNoLimit(): void
{
$limits = BudgetLimit::whereNull('period')->get();
/** @var BudgetLimit $limit */
foreach ($limits as $limit) {
$this->fixLimit($limit);
}
}
} }

View File

@@ -32,7 +32,6 @@ use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface; use Psr\Container\NotFoundExceptionInterface;
@@ -87,54 +86,6 @@ class BackToJournals extends Command
return 0; return 0;
} }
/**
* @return array
*/
private function getIdsForBudgets(): array
{
$transactions = DB::table('budget_transaction')->distinct()->pluck('transaction_id')->toArray();
$array = [];
$chunks = array_chunk($transactions, 500);
foreach ($chunks as $chunk) {
$set = DB::table('transactions')->whereIn('transactions.id', $chunk)->pluck('transaction_journal_id')->toArray();
$array = array_merge($array, $set);
}
return $array;
}
/**
* @return array
*/
private function getIdsForCategories(): array
{
$transactions = DB::table('category_transaction')->distinct()->pluck('transaction_id')->toArray();
$array = [];
$chunks = array_chunk($transactions, 500);
foreach ($chunks as $chunk) {
$set = DB::table('transactions')
->whereIn('transactions.id', $chunk)
->pluck('transaction_journal_id')->toArray();
$array = array_merge($array, $set);
}
return $array;
}
/**
* @return bool
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function isExecuted(): bool
{
$configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false);
return (bool)$configVar->data;
}
/** /**
* @return bool * @return bool
* @throws ContainerExceptionInterface * @throws ContainerExceptionInterface
@@ -148,11 +99,15 @@ class BackToJournals extends Command
} }
/** /**
* * @return bool
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/ */
private function markAsExecuted(): void private function isExecuted(): bool
{ {
app('fireflyconfig')->set(self::CONFIG_NAME, true); $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false);
return (bool)$configVar->data;
} }
/** /**
@@ -186,6 +141,23 @@ class BackToJournals extends Command
} }
} }
/**
* @return array
*/
private function getIdsForBudgets(): array
{
$transactions = DB::table('budget_transaction')->distinct()->pluck('transaction_id')->toArray();
$array = [];
$chunks = array_chunk($transactions, 500);
foreach ($chunks as $chunk) {
$set = DB::table('transactions')->whereIn('transactions.id', $chunk)->pluck('transaction_journal_id')->toArray();
$array = array_merge($array, $set);
}
return $array;
}
/** /**
* @param TransactionJournal $journal * @param TransactionJournal $journal
*/ */
@@ -239,6 +211,25 @@ class BackToJournals extends Command
} }
} }
/**
* @return array
*/
private function getIdsForCategories(): array
{
$transactions = DB::table('category_transaction')->distinct()->pluck('transaction_id')->toArray();
$array = [];
$chunks = array_chunk($transactions, 500);
foreach ($chunks as $chunk) {
$set = DB::table('transactions')
->whereIn('transactions.id', $chunk)
->pluck('transaction_journal_id')->toArray();
$array = array_merge($array, $set);
}
return $array;
}
/** /**
* @param TransactionJournal $journal * @param TransactionJournal $journal
*/ */
@@ -268,4 +259,12 @@ class BackToJournals extends Command
$journal->categories()->sync([(int)$category->id]); $journal->categories()->sync([(int)$category->id]);
} }
} }
/**
*
*/
private function markAsExecuted(): void
{
app('fireflyconfig')->set(self::CONFIG_NAME, true);
}
} }

View File

@@ -77,81 +77,6 @@ class DecryptDatabase extends Command
return 0; return 0;
} }
/**
* @param string $table
* @param string $field
*/
private function decryptField(string $table, string $field): void
{
$rows = DB::table($table)->get(['id', $field]);
/** @var stdClass $row */
foreach ($rows as $row) {
$this->decryptRow($table, $field, $row);
}
}
/**
* @param int $id
* @param string $value
*/
private function decryptPreferencesRow(int $id, string $value): void
{
// try to json_decrypt the value.
try {
$newValue = json_decode($value, true, 512, JSON_THROW_ON_ERROR) ?? $value;
} catch (JsonException $e) {
$message = sprintf('Could not JSON decode preference row #%d: %s. This does not have to be a problem.', $id, $e->getMessage());
$this->friendlyError($message);
app('log')->warning($message);
app('log')->warning($value);
app('log')->warning($e->getTraceAsString());
return;
}
/** @var Preference $object */
$object = Preference::find((int)$id);
if (null !== $object) {
$object->data = $newValue;
$object->save();
}
}
/**
* @param string $table
* @param string $field
* @param stdClass $row
*/
private function decryptRow(string $table, string $field, stdClass $row): void
{
$original = $row->$field;
if (null === $original) {
return;
}
$id = (int)$row->id;
$value = '';
try {
$value = $this->tryDecrypt($original);
} catch (FireflyException $e) {
$message = sprintf('Could not decrypt field "%s" in row #%d of table "%s": %s', $field, $id, $table, $e->getMessage());
$this->friendlyError($message);
Log::error($message);
Log::error($e->getTraceAsString());
}
// A separate routine for preferences table:
if ('preferences' === $table) {
$this->decryptPreferencesRow($id, $value);
return;
}
if ($value !== $original) {
DB::table($table)->where('id', $id)->update([$field => $value]);
}
}
/** /**
* @param string $table * @param string $table
* @param array $fields * @param array $fields
@@ -198,6 +123,54 @@ class DecryptDatabase extends Command
return false; return false;
} }
/**
* @param string $table
* @param string $field
*/
private function decryptField(string $table, string $field): void
{
$rows = DB::table($table)->get(['id', $field]);
/** @var stdClass $row */
foreach ($rows as $row) {
$this->decryptRow($table, $field, $row);
}
}
/**
* @param string $table
* @param string $field
* @param stdClass $row
*/
private function decryptRow(string $table, string $field, stdClass $row): void
{
$original = $row->$field;
if (null === $original) {
return;
}
$id = (int)$row->id;
$value = '';
try {
$value = $this->tryDecrypt($original);
} catch (FireflyException $e) {
$message = sprintf('Could not decrypt field "%s" in row #%d of table "%s": %s', $field, $id, $table, $e->getMessage());
$this->friendlyError($message);
Log::error($message);
Log::error($e->getTraceAsString());
}
// A separate routine for preferences table:
if ('preferences' === $table) {
$this->decryptPreferencesRow($id, $value);
return;
}
if ($value !== $original) {
DB::table($table)->where('id', $id)->update([$field => $value]);
}
}
/** /**
* Tries to decrypt data. Will only throw an exception when the MAC is invalid. * Tries to decrypt data. Will only throw an exception when the MAC is invalid.
* *
@@ -218,4 +191,31 @@ class DecryptDatabase extends Command
return $value; return $value;
} }
/**
* @param int $id
* @param string $value
*/
private function decryptPreferencesRow(int $id, string $value): void
{
// try to json_decrypt the value.
try {
$newValue = json_decode($value, true, 512, JSON_THROW_ON_ERROR) ?? $value;
} catch (JsonException $e) {
$message = sprintf('Could not JSON decode preference row #%d: %s. This does not have to be a problem.', $id, $e->getMessage());
$this->friendlyError($message);
app('log')->warning($message);
app('log')->warning($value);
app('log')->warning($e->getTraceAsString());
return;
}
/** @var Preference $object */
$object = Preference::find((int)$id);
if (null !== $object) {
$object->data = $newValue;
$object->save();
}
}
} }

View File

@@ -101,11 +101,20 @@ class MigrateRecurrenceMeta extends Command
} }
/** /**
* * @return int
* @throws JsonException
*/ */
private function markAsExecuted(): void private function migrateMetaData(): int
{ {
app('fireflyconfig')->set(self::CONFIG_NAME, true); $count = 0;
// get all recurrence meta data:
$collection = RecurrenceMeta::with('recurrence')->get();
/** @var RecurrenceMeta $meta */
foreach ($collection as $meta) {
$count += $this->migrateEntry($meta);
}
return $count;
} }
/** /**
@@ -145,19 +154,10 @@ class MigrateRecurrenceMeta extends Command
} }
/** /**
* @return int *
* @throws JsonException
*/ */
private function migrateMetaData(): int private function markAsExecuted(): void
{ {
$count = 0; app('fireflyconfig')->set(self::CONFIG_NAME, true);
// get all recurrence meta data:
$collection = RecurrenceMeta::with('recurrence')->get();
/** @var RecurrenceMeta $meta */
foreach ($collection as $meta) {
$count += $this->migrateEntry($meta);
}
return $count;
} }
} }

View File

@@ -77,14 +77,6 @@ class MigrateRecurrenceType extends Command
return 0; return 0;
} }
/**
*
*/
private function getInvalidType(): TransactionType
{
return TransactionType::whereType(TransactionType::INVALID)->firstOrCreate(['type' => TransactionType::INVALID]);
}
/** /**
* @return bool * @return bool
* @throws ContainerExceptionInterface * @throws ContainerExceptionInterface
@@ -99,9 +91,15 @@ class MigrateRecurrenceType extends Command
/** /**
* *
*/ */
private function markAsExecuted(): void private function migrateTypes(): void
{ {
app('fireflyconfig')->set(self::CONFIG_NAME, true); $set = Recurrence::get();
/** @var Recurrence $recurrence */
foreach ($set as $recurrence) {
if ($recurrence->transactionType->type !== TransactionType::INVALID) {
$this->migrateRecurrence($recurrence);
}
}
} }
/** /**
@@ -125,14 +123,16 @@ class MigrateRecurrenceType extends Command
/** /**
* *
*/ */
private function migrateTypes(): void private function getInvalidType(): TransactionType
{ {
$set = Recurrence::get(); return TransactionType::whereType(TransactionType::INVALID)->firstOrCreate(['type' => TransactionType::INVALID]);
/** @var Recurrence $recurrence */ }
foreach ($set as $recurrence) {
if ($recurrence->transactionType->type !== TransactionType::INVALID) { /**
$this->migrateRecurrence($recurrence); *
} */
} private function markAsExecuted(): void
{
app('fireflyconfig')->set(self::CONFIG_NAME, true);
} }
} }

View File

@@ -74,16 +74,6 @@ class MigrateTagLocations extends Command
return 0; return 0;
} }
/**
* @param Tag $tag
*
* @return bool
*/
private function hasLocationDetails(Tag $tag): bool
{
return null !== $tag->latitude && null !== $tag->longitude && null !== $tag->zoomLevel;
}
/** /**
* @return bool * @return bool
* @throws ContainerExceptionInterface * @throws ContainerExceptionInterface
@@ -99,12 +89,25 @@ class MigrateTagLocations extends Command
return false; return false;
} }
/** private function migrateTagLocations(): void
*
*/
private function markAsExecuted(): void
{ {
app('fireflyconfig')->set(self::CONFIG_NAME, true); $tags = Tag::get();
/** @var Tag $tag */
foreach ($tags as $tag) {
if ($this->hasLocationDetails($tag)) {
$this->migrateLocationDetails($tag);
}
}
}
/**
* @param Tag $tag
*
* @return bool
*/
private function hasLocationDetails(Tag $tag): bool
{
return null !== $tag->latitude && null !== $tag->longitude && null !== $tag->zoomLevel;
} }
/** /**
@@ -125,14 +128,11 @@ class MigrateTagLocations extends Command
$tag->save(); $tag->save();
} }
private function migrateTagLocations(): void /**
*
*/
private function markAsExecuted(): void
{ {
$tags = Tag::get(); app('fireflyconfig')->set(self::CONFIG_NAME, true);
/** @var Tag $tag */
foreach ($tags as $tag) {
if ($this->hasLocationDetails($tag)) {
$this->migrateLocationDetails($tag);
}
}
} }
} }

View File

@@ -26,7 +26,6 @@ namespace FireflyIII\Console\Commands\Upgrade;
use DB; use DB;
use Exception; use Exception;
use FireflyIII\Console\Commands\ShowsFriendlyMessages; use FireflyIII\Console\Commands\ShowsFriendlyMessages;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\TransactionGroupFactory; use FireflyIII\Factory\TransactionGroupFactory;
use FireflyIII\Models\Budget; use FireflyIII\Models\Budget;
use FireflyIII\Models\Category; use FireflyIII\Models\Category;
@@ -96,122 +95,19 @@ class MigrateToGroups extends Command
} }
/** /**
* @param TransactionJournal $journal * Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* @param Transaction $transaction * executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
* *
* @return Transaction|null
*/ */
private function findOpposingTransaction(TransactionJournal $journal, Transaction $transaction): ?Transaction private function stupidLaravel(): void
{ {
$set = $journal->transactions->filter( $this->count = 0;
static function (Transaction $subject) use ($transaction) { $this->journalRepository = app(JournalRepositoryInterface::class);
$amount = (float)$transaction->amount * -1 === (float)$subject->amount; // intentional float $this->service = app(JournalDestroyService::class);
$identifier = $transaction->identifier === $subject->identifier; $this->groupFactory = app(TransactionGroupFactory::class);
Log::debug(sprintf('Amount the same? %s', var_export($amount, true))); $this->cliRepository = app(JournalCLIRepositoryInterface::class);
Log::debug(sprintf('ID the same? %s', var_export($identifier, true)));
return $amount && $identifier;
}
);
return $set->first();
}
/**
* @param TransactionJournal $journal
*
* @return Collection
*/
private function getDestinationTransactions(TransactionJournal $journal): Collection
{
return $journal->transactions->filter(
static function (Transaction $transaction) {
return $transaction->amount > 0;
}
);
}
/**
* @param Transaction $left
* @param Transaction $right
*
* @return int|null
*/
private function getTransactionBudget(Transaction $left, Transaction $right): ?int
{
Log::debug('Now in getTransactionBudget()');
// try to get a budget ID from the left transaction:
/** @var Budget|null $budget */
$budget = $left->budgets()->first();
if (null !== $budget) {
Log::debug(sprintf('Return budget #%d, from transaction #%d', $budget->id, $left->id));
return (int)$budget->id;
}
// try to get a budget ID from the right transaction:
/** @var Budget|null $budget */
$budget = $right->budgets()->first();
if (null !== $budget) {
Log::debug(sprintf('Return budget #%d, from transaction #%d', $budget->id, $right->id));
return (int)$budget->id;
}
Log::debug('Neither left or right have a budget, return NULL');
// if all fails, return NULL.
return null;
}
/**
* @param Transaction $left
* @param Transaction $right
*
* @return int|null
*/
private function getTransactionCategory(Transaction $left, Transaction $right): ?int
{
Log::debug('Now in getTransactionCategory()');
// try to get a category ID from the left transaction:
/** @var Category|null $category */
$category = $left->categories()->first();
if (null !== $category) {
Log::debug(sprintf('Return category #%d, from transaction #%d', $category->id, $left->id));
return (int)$category->id;
}
// try to get a category ID from the left transaction:
/** @var Category|null $category */
$category = $right->categories()->first();
if (null !== $category) {
Log::debug(sprintf('Return category #%d, from transaction #%d', $category->id, $category->id));
return (int)$category->id;
}
Log::debug('Neither left or right have a category, return NULL');
// if all fails, return NULL.
return null;
}
/**
* @param array $array
*/
private function giveGroup(array $array): void
{
$groupId = DB::table('transaction_groups')->insertGetId(
[
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'title' => null,
'user_id' => $array['user_id'],
]
);
DB::table('transaction_journals')->where('id', $array['id'])->update(['transaction_group_id' => $groupId]);
$this->count++;
} }
/** /**
@@ -229,26 +125,6 @@ class MigrateToGroups extends Command
return false; return false;
} }
/**
* Gives all journals without a group a group.
*/
private function makeGroupsFromAll(): void
{
$orphanedJournals = $this->cliRepository->getJournalsWithoutGroup();
$total = count($orphanedJournals);
if ($total > 0) {
Log::debug(sprintf('Going to convert %d transaction journals. Please hold..', $total));
$this->friendlyInfo(sprintf('Going to convert %d transaction journals. Please hold..', $total));
/** @var array $array */
foreach ($orphanedJournals as $array) {
$this->giveGroup($array);
}
}
if (0 === $total) {
$this->friendlyPositive('No need to convert transaction journals.');
}
}
/** /**
* @throws Exception * @throws Exception
*/ */
@@ -406,6 +282,145 @@ class MigrateToGroups extends Command
); );
} }
/**
* @param TransactionJournal $journal
*
* @return Collection
*/
private function getDestinationTransactions(TransactionJournal $journal): Collection
{
return $journal->transactions->filter(
static function (Transaction $transaction) {
return $transaction->amount > 0;
}
);
}
/**
* @param TransactionJournal $journal
* @param Transaction $transaction
*
* @return Transaction|null
*/
private function findOpposingTransaction(TransactionJournal $journal, Transaction $transaction): ?Transaction
{
$set = $journal->transactions->filter(
static function (Transaction $subject) use ($transaction) {
$amount = (float)$transaction->amount * -1 === (float)$subject->amount; // intentional float
$identifier = $transaction->identifier === $subject->identifier;
Log::debug(sprintf('Amount the same? %s', var_export($amount, true)));
Log::debug(sprintf('ID the same? %s', var_export($identifier, true)));
return $amount && $identifier;
}
);
return $set->first();
}
/**
* @param Transaction $left
* @param Transaction $right
*
* @return int|null
*/
private function getTransactionBudget(Transaction $left, Transaction $right): ?int
{
Log::debug('Now in getTransactionBudget()');
// try to get a budget ID from the left transaction:
/** @var Budget|null $budget */
$budget = $left->budgets()->first();
if (null !== $budget) {
Log::debug(sprintf('Return budget #%d, from transaction #%d', $budget->id, $left->id));
return (int)$budget->id;
}
// try to get a budget ID from the right transaction:
/** @var Budget|null $budget */
$budget = $right->budgets()->first();
if (null !== $budget) {
Log::debug(sprintf('Return budget #%d, from transaction #%d', $budget->id, $right->id));
return (int)$budget->id;
}
Log::debug('Neither left or right have a budget, return NULL');
// if all fails, return NULL.
return null;
}
/**
* @param Transaction $left
* @param Transaction $right
*
* @return int|null
*/
private function getTransactionCategory(Transaction $left, Transaction $right): ?int
{
Log::debug('Now in getTransactionCategory()');
// try to get a category ID from the left transaction:
/** @var Category|null $category */
$category = $left->categories()->first();
if (null !== $category) {
Log::debug(sprintf('Return category #%d, from transaction #%d', $category->id, $left->id));
return (int)$category->id;
}
// try to get a category ID from the left transaction:
/** @var Category|null $category */
$category = $right->categories()->first();
if (null !== $category) {
Log::debug(sprintf('Return category #%d, from transaction #%d', $category->id, $category->id));
return (int)$category->id;
}
Log::debug('Neither left or right have a category, return NULL');
// if all fails, return NULL.
return null;
}
/**
* Gives all journals without a group a group.
*/
private function makeGroupsFromAll(): void
{
$orphanedJournals = $this->cliRepository->getJournalsWithoutGroup();
$total = count($orphanedJournals);
if ($total > 0) {
Log::debug(sprintf('Going to convert %d transaction journals. Please hold..', $total));
$this->friendlyInfo(sprintf('Going to convert %d transaction journals. Please hold..', $total));
/** @var array $array */
foreach ($orphanedJournals as $array) {
$this->giveGroup($array);
}
}
if (0 === $total) {
$this->friendlyPositive('No need to convert transaction journals.');
}
}
/**
* @param array $array
*/
private function giveGroup(array $array): void
{
$groupId = DB::table('transaction_groups')->insertGetId(
[
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'title' => null,
'user_id' => $array['user_id'],
]
);
DB::table('transaction_journals')->where('id', $array['id'])->update(['transaction_group_id' => $groupId]);
$this->count++;
}
/** /**
* *
*/ */
@@ -413,20 +428,4 @@ class MigrateToGroups extends Command
{ {
app('fireflyconfig')->set(self::CONFIG_NAME, true); app('fireflyconfig')->set(self::CONFIG_NAME, true);
} }
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*
*/
private function stupidLaravel(): void
{
$this->count = 0;
$this->journalRepository = app(JournalRepositoryInterface::class);
$this->service = app(JournalDestroyService::class);
$this->groupFactory = app(TransactionGroupFactory::class);
$this->cliRepository = app(JournalCLIRepositoryInterface::class);
}
} }

View File

@@ -104,6 +104,22 @@ class MigrateToRules extends Command
return 0; return 0;
} }
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*
*/
private function stupidLaravel(): void
{
$this->count = 0;
$this->userRepository = app(UserRepositoryInterface::class);
$this->ruleGroupRepository = app(RuleGroupRepositoryInterface::class);
$this->billRepository = app(BillRepositoryInterface::class);
$this->ruleRepository = app(RuleRepositoryInterface::class);
}
/** /**
* @return bool * @return bool
* @throws ContainerExceptionInterface * @throws ContainerExceptionInterface
@@ -120,11 +136,38 @@ class MigrateToRules extends Command
} }
/** /**
* Migrate bills to new rule structure for a specific user.
* *
* @param User $user
*
* @throws FireflyException
*/ */
private function markAsExecuted(): void private function migrateUser(User $user): void
{ {
app('fireflyconfig')->set(self::CONFIG_NAME, true); $this->ruleGroupRepository->setUser($user);
$this->billRepository->setUser($user);
$this->ruleRepository->setUser($user);
/** @var Preference $lang */
$lang = app('preferences')->getForUser($user, 'language', 'en_US');
$groupTitle = (string)trans('firefly.rulegroup_for_bills_title', [], $lang->data);
$ruleGroup = $this->ruleGroupRepository->findByTitle($groupTitle);
if (null === $ruleGroup) {
$ruleGroup = $this->ruleGroupRepository->store(
[
'title' => (string)trans('firefly.rulegroup_for_bills_title', [], $lang->data),
'description' => (string)trans('firefly.rulegroup_for_bills_description', [], $lang->data),
'active' => true,
]
);
}
$bills = $this->billRepository->getBills();
/** @var Bill $bill */
foreach ($bills as $bill) {
$this->migrateBill($ruleGroup, $bill, $lang);
}
} }
/** /**
@@ -199,53 +242,10 @@ class MigrateToRules extends Command
} }
/** /**
* Migrate bills to new rule structure for a specific user.
* *
* @param User $user
*
* @throws FireflyException
*/ */
private function migrateUser(User $user): void private function markAsExecuted(): void
{ {
$this->ruleGroupRepository->setUser($user); app('fireflyconfig')->set(self::CONFIG_NAME, true);
$this->billRepository->setUser($user);
$this->ruleRepository->setUser($user);
/** @var Preference $lang */
$lang = app('preferences')->getForUser($user, 'language', 'en_US');
$groupTitle = (string)trans('firefly.rulegroup_for_bills_title', [], $lang->data);
$ruleGroup = $this->ruleGroupRepository->findByTitle($groupTitle);
if (null === $ruleGroup) {
$ruleGroup = $this->ruleGroupRepository->store(
[
'title' => (string)trans('firefly.rulegroup_for_bills_title', [], $lang->data),
'description' => (string)trans('firefly.rulegroup_for_bills_description', [], $lang->data),
'active' => true,
]
);
}
$bills = $this->billRepository->getBills();
/** @var Bill $bill */
foreach ($bills as $bill) {
$this->migrateBill($ruleGroup, $bill, $lang);
}
}
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*
*/
private function stupidLaravel(): void
{
$this->count = 0;
$this->userRepository = app(UserRepositoryInterface::class);
$this->ruleGroupRepository = app(RuleGroupRepositoryInterface::class);
$this->billRepository = app(BillRepositoryInterface::class);
$this->ruleRepository = app(RuleRepositoryInterface::class);
} }
} }

View File

@@ -84,72 +84,20 @@ class OtherCurrenciesCorrections extends Command
} }
/** /**
* @param Account $account * Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
* *
* @return TransactionCurrency|null
*/ */
private function getCurrency(Account $account): ?TransactionCurrency private function stupidLaravel(): void
{ {
$accountId = $account->id; $this->count = 0;
if (array_key_exists($accountId, $this->accountCurrencies) && 0 === $this->accountCurrencies[$accountId]) { $this->accountCurrencies = [];
return null; $this->accountRepos = app(AccountRepositoryInterface::class);
} $this->currencyRepos = app(CurrencyRepositoryInterface::class);
if (array_key_exists($accountId, $this->accountCurrencies) && $this->accountCurrencies[$accountId] instanceof TransactionCurrency) { $this->journalRepos = app(JournalRepositoryInterface::class);
return $this->accountCurrencies[$accountId]; $this->cliRepos = app(JournalCLIRepositoryInterface::class);
}
$currency = $this->accountRepos->getAccountCurrency($account);
if (null === $currency) {
$this->accountCurrencies[$accountId] = 0;
return null;
}
$this->accountCurrencies[$accountId] = $currency;
return $currency;
}
/**
* Gets the transaction that determines the transaction that "leads" and will determine
* the currency to be used by all transactions, and the journal itself.
*
* @param TransactionJournal $journal
*
* @return Transaction|null
*/
private function getLeadTransaction(TransactionJournal $journal): ?Transaction
{
/** @var Transaction $lead */
$lead = null;
switch ($journal->transactionType->type) {
default:
break;
case TransactionType::WITHDRAWAL:
$lead = $journal->transactions()->where('amount', '<', 0)->first();
break;
case TransactionType::DEPOSIT:
$lead = $journal->transactions()->where('amount', '>', 0)->first();
break;
case TransactionType::OPENING_BALANCE:
// whichever isn't an initial balance account:
$lead = $journal->transactions()->leftJoin('accounts', 'transactions.account_id', '=', 'accounts.id')->leftJoin(
'account_types',
'accounts.account_type_id',
'=',
'account_types.id'
)->where('account_types.type', '!=', AccountType::INITIAL_BALANCE)->first(['transactions.*']);
break;
case TransactionType::RECONCILIATION:
// whichever isn't the reconciliation account:
$lead = $journal->transactions()->leftJoin('accounts', 'transactions.account_id', '=', 'accounts.id')->leftJoin(
'account_types',
'accounts.account_type_id',
'=',
'account_types.id'
)->where('account_types.type', '!=', AccountType::RECONCILIATION)->first(['transactions.*']);
break;
}
return $lead;
} }
/** /**
@@ -168,28 +116,21 @@ class OtherCurrenciesCorrections extends Command
} }
/** /**
* * This routine verifies that withdrawals, deposits and opening balances have the correct currency settings for
* the accounts they are linked to.
* Both source and destination must match the respective currency preference of the related asset account.
* So FF3 must verify all transactions.
*/ */
private function markAsExecuted(): void private function updateOtherJournalsCurrencies(): void
{ {
app('fireflyconfig')->set(self::CONFIG_NAME, true); $set = $this->cliRepos->getAllJournals(
[TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::OPENING_BALANCE, TransactionType::RECONCILIATION,]
);
/** @var TransactionJournal $journal */
foreach ($set as $journal) {
$this->updateJournalCurrency($journal);
} }
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*
*/
private function stupidLaravel(): void
{
$this->count = 0;
$this->accountCurrencies = [];
$this->accountRepos = app(AccountRepositoryInterface::class);
$this->currencyRepos = app(CurrencyRepositoryInterface::class);
$this->journalRepos = app(JournalRepositoryInterface::class);
$this->cliRepos = app(JournalCLIRepositoryInterface::class);
} }
/** /**
@@ -249,20 +190,79 @@ class OtherCurrenciesCorrections extends Command
} }
/** /**
* This routine verifies that withdrawals, deposits and opening balances have the correct currency settings for * Gets the transaction that determines the transaction that "leads" and will determine
* the accounts they are linked to. * the currency to be used by all transactions, and the journal itself.
* Both source and destination must match the respective currency preference of the related asset account. *
* So FF3 must verify all transactions. * @param TransactionJournal $journal
*
* @return Transaction|null
*/ */
private function updateOtherJournalsCurrencies(): void private function getLeadTransaction(TransactionJournal $journal): ?Transaction
{ {
$set = $this->cliRepos->getAllJournals( /** @var Transaction $lead */
[TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::OPENING_BALANCE, TransactionType::RECONCILIATION,] $lead = null;
); switch ($journal->transactionType->type) {
default:
break;
case TransactionType::WITHDRAWAL:
$lead = $journal->transactions()->where('amount', '<', 0)->first();
break;
case TransactionType::DEPOSIT:
$lead = $journal->transactions()->where('amount', '>', 0)->first();
break;
case TransactionType::OPENING_BALANCE:
// whichever isn't an initial balance account:
$lead = $journal->transactions()->leftJoin('accounts', 'transactions.account_id', '=', 'accounts.id')->leftJoin(
'account_types',
'accounts.account_type_id',
'=',
'account_types.id'
)->where('account_types.type', '!=', AccountType::INITIAL_BALANCE)->first(['transactions.*']);
break;
case TransactionType::RECONCILIATION:
// whichever isn't the reconciliation account:
$lead = $journal->transactions()->leftJoin('accounts', 'transactions.account_id', '=', 'accounts.id')->leftJoin(
'account_types',
'accounts.account_type_id',
'=',
'account_types.id'
)->where('account_types.type', '!=', AccountType::RECONCILIATION)->first(['transactions.*']);
break;
}
/** @var TransactionJournal $journal */ return $lead;
foreach ($set as $journal) { }
$this->updateJournalCurrency($journal);
} /**
* @param Account $account
*
* @return TransactionCurrency|null
*/
private function getCurrency(Account $account): ?TransactionCurrency
{
$accountId = $account->id;
if (array_key_exists($accountId, $this->accountCurrencies) && 0 === $this->accountCurrencies[$accountId]) {
return null;
}
if (array_key_exists($accountId, $this->accountCurrencies) && $this->accountCurrencies[$accountId] instanceof TransactionCurrency) {
return $this->accountCurrencies[$accountId];
}
$currency = $this->accountRepos->getAccountCurrency($account);
if (null === $currency) {
$this->accountCurrencies[$accountId] = 0;
return null;
}
$this->accountCurrencies[$accountId] = $currency;
return $currency;
}
/**
*
*/
private function markAsExecuted(): void
{
app('fireflyconfig')->set(self::CONFIG_NAME, true);
} }
} }

View File

@@ -49,13 +49,14 @@ class TransactionIdentifier extends Command
private int $count; private int $count;
/** /**
* This method gives all transactions which are part of a split journal (so more than 2) a sort of "order" so they are easier * This method gives all transactions which are part of a split journal (so more than 2) a sort of "order" so they
* to easier to match to their counterpart. When a journal is split, it has two or three transactions: -3, -4 and -5 for example. * are easier to easier to match to their counterpart. When a journal is split, it has two or three transactions:
* -3, -4 and -5 for example.
* *
* In the database this is reflected as 6 transactions: -3/+3, -4/+4, -5/+5. * In the database this is reflected as 6 transactions: -3/+3, -4/+4, -5/+5.
* *
* When either of these are the same amount, FF3 can't keep them apart: +3/-3, +3/-3, +3/-3. This happens more often than you would * When either of these are the same amount, FF3 can't keep them apart: +3/-3, +3/-3, +3/-3. This happens more
* think. So each set gets a number (1,2,3) to keep them apart. * often than you would think. So each set gets a number (1,2,3) to keep them apart.
* *
* @return int * @return int
* @throws ContainerExceptionInterface * @throws ContainerExceptionInterface
@@ -95,6 +96,63 @@ class TransactionIdentifier extends Command
return 0; return 0;
} }
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*
*/
private function stupidLaravel(): void
{
$this->cliRepository = app(JournalCLIRepositoryInterface::class);
$this->count = 0;
}
/**
* @return bool
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function isExecuted(): bool
{
$configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false);
if (null !== $configVar) {
return (bool)$configVar->data;
}
return false;
}
/**
* Grab all positive transactions from this journal that are not deleted. for each one, grab the negative opposing
* one which has 0 as an identifier and give it the same identifier.
*
* @param TransactionJournal $transactionJournal
*/
private function updateJournalIdentifiers(TransactionJournal $transactionJournal): void
{
$identifier = 0;
$exclude = []; // transactions already processed.
$transactions = $transactionJournal->transactions()->where('amount', '>', 0)->get();
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
$opposing = $this->findOpposing($transaction, $exclude);
if (null !== $opposing) {
// give both a new identifier:
$transaction->identifier = $identifier;
$opposing->identifier = $identifier;
$transaction->save();
$opposing->save();
$exclude[] = $transaction->id;
$exclude[] = $opposing->id;
$this->count++;
}
++$identifier;
}
}
/** /**
* @param Transaction $transaction * @param Transaction $transaction
* @param array $exclude * @param array $exclude
@@ -125,21 +183,6 @@ class TransactionIdentifier extends Command
return $opposing; return $opposing;
} }
/**
* @return bool
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function isExecuted(): bool
{
$configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false);
if (null !== $configVar) {
return (bool)$configVar->data;
}
return false;
}
/** /**
* *
*/ */
@@ -147,46 +190,4 @@ class TransactionIdentifier extends Command
{ {
app('fireflyconfig')->set(self::CONFIG_NAME, true); app('fireflyconfig')->set(self::CONFIG_NAME, true);
} }
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*
*/
private function stupidLaravel(): void
{
$this->cliRepository = app(JournalCLIRepositoryInterface::class);
$this->count = 0;
}
/**
* Grab all positive transactions from this journal that are not deleted. for each one, grab the negative opposing one
* which has 0 as an identifier and give it the same identifier.
*
* @param TransactionJournal $transactionJournal
*/
private function updateJournalIdentifiers(TransactionJournal $transactionJournal): void
{
$identifier = 0;
$exclude = []; // transactions already processed.
$transactions = $transactionJournal->transactions()->where('amount', '>', 0)->get();
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
$opposing = $this->findOpposing($transaction, $exclude);
if (null !== $opposing) {
// give both a new identifier:
$transaction->identifier = $identifier;
$opposing->identifier = $identifier;
$transaction->save();
$opposing->save();
$exclude[] = $transaction->id;
$exclude[] = $opposing->id;
$this->count++;
}
++$identifier;
}
}
} }

View File

@@ -86,6 +86,304 @@ class TransferCurrenciesCorrections extends Command
return 0; return 0;
} }
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*
*/
private function stupidLaravel(): void
{
$this->count = 0;
$this->accountRepos = app(AccountRepositoryInterface::class);
$this->cliRepos = app(JournalCLIRepositoryInterface::class);
$this->accountCurrencies = [];
$this->resetInformation();
}
/**
* Reset all the class fields for the current transfer.
*
*/
private function resetInformation(): void
{
$this->sourceTransaction = null;
$this->sourceAccount = null;
$this->sourceCurrency = null;
$this->destinationTransaction = null;
$this->destinationAccount = null;
$this->destinationCurrency = null;
}
/**
* @return bool
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function isExecuted(): bool
{
$configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false);
if (null !== $configVar) {
return (bool)$configVar->data;
}
return false;
}
/**
* This routine verifies that transfers have the correct currency settings for the accounts they are linked to.
* For transfers, this is can be a destructive routine since we FORCE them into a currency setting whether they
* like it or not. Previous routines MUST have set the currency setting for both accounts for this to work.
*
* Both source and destination must match the respective currency preference. So FF3 must verify ALL
* transactions.
*/
private function startUpdateRoutine(): void
{
$set = $this->cliRepos->getAllJournals([TransactionType::TRANSFER]);
/** @var TransactionJournal $journal */
foreach ($set as $journal) {
$this->updateTransferCurrency($journal);
}
}
/**
* @param TransactionJournal $transfer
*/
private function updateTransferCurrency(TransactionJournal $transfer): void
{
$this->resetInformation();
if ($this->isSplitJournal($transfer)) {
$this->friendlyWarning(sprintf('Transaction journal #%d is a split journal. Cannot continue.', $transfer->id));
return;
}
$this->getSourceInformation($transfer);
$this->getDestinationInformation($transfer);
// unexpectedly, either one is null:
if ($this->isEmptyTransactions()) {
$this->friendlyError(sprintf('Source or destination information for transaction journal #%d is null. Cannot fix this one.', $transfer->id));
return;
}
// both accounts must have currency preference:
if ($this->isNoCurrencyPresent()) {
$this->friendlyError(
sprintf('Source or destination accounts for transaction journal #%d have no currency information. Cannot fix this one.', $transfer->id)
);
return;
}
// fix source transaction having no currency.
$this->fixSourceNoCurrency();
// fix source transaction having bad currency.
$this->fixSourceUnmatchedCurrency();
// fix destination transaction having no currency.
$this->fixDestNoCurrency();
// fix destination transaction having bad currency.
$this->fixDestinationUnmatchedCurrency();
// remove foreign currency information if not necessary.
$this->fixInvalidForeignCurrency();
// correct foreign currency info if necessary.
$this->fixMismatchedForeignCurrency();
// restore missing foreign currency amount.
$this->fixSourceNullForeignAmount();
$this->fixDestNullForeignAmount();
// fix journal itself:
$this->fixTransactionJournalCurrency($transfer);
}
/**
* Is this a split transaction journal?
*
* @param TransactionJournal $transfer
*
* @return bool
*/
private function isSplitJournal(TransactionJournal $transfer): bool
{
return $transfer->transactions->count() > 2;
}
/**
* Extract source transaction, source account + source account currency from the journal.
*
* @param TransactionJournal $journal
*
*/
private function getSourceInformation(TransactionJournal $journal): void
{
$this->sourceTransaction = $this->getSourceTransaction($journal);
$this->sourceAccount = $this->sourceTransaction?->account;
$this->sourceCurrency = null === $this->sourceAccount ? null : $this->getCurrency($this->sourceAccount);
}
/**
* @param TransactionJournal $transfer
*
* @return Transaction|null
*/
private function getSourceTransaction(TransactionJournal $transfer): ?Transaction
{
return $transfer->transactions()->where('amount', '<', 0)->first();
}
/**
* @param Account $account
*
* @return TransactionCurrency|null
*/
private function getCurrency(Account $account): ?TransactionCurrency
{
$accountId = $account->id;
if (array_key_exists($accountId, $this->accountCurrencies) && 0 === $this->accountCurrencies[$accountId]) {
return null;
}
if (array_key_exists($accountId, $this->accountCurrencies) && $this->accountCurrencies[$accountId] instanceof TransactionCurrency) {
return $this->accountCurrencies[$accountId];
}
$currency = $this->accountRepos->getAccountCurrency($account);
if (null === $currency) {
$this->accountCurrencies[$accountId] = 0;
return null;
}
$this->accountCurrencies[$accountId] = $currency;
return $currency;
}
/**
* Extract destination transaction, destination account + destination account currency from the journal.
*
* @param TransactionJournal $journal
*
*/
private function getDestinationInformation(TransactionJournal $journal): void
{
$this->destinationTransaction = $this->getDestinationTransaction($journal);
$this->destinationAccount = $this->destinationTransaction?->account;
$this->destinationCurrency = null === $this->destinationAccount ? null : $this->getCurrency($this->destinationAccount);
}
/**
* @param TransactionJournal $transfer
*
* @return Transaction|null
*/
private function getDestinationTransaction(TransactionJournal $transfer): ?Transaction
{
return $transfer->transactions()->where('amount', '>', 0)->first();
}
/**
* Is either the source or destination transaction NULL?
*
* @return bool
*/
private function isEmptyTransactions(): bool
{
return null === $this->sourceTransaction || null === $this->destinationTransaction
|| null === $this->sourceAccount
|| null === $this->destinationAccount;
}
/**
* @return bool
*/
private function isNoCurrencyPresent(): bool
{
// source account must have a currency preference.
if (null === $this->sourceCurrency) {
$message = sprintf('Account #%d ("%s") must have currency preference but has none.', $this->sourceAccount->id, $this->sourceAccount->name);
Log::error($message);
$this->friendlyError($message);
return true;
}
// destination account must have a currency preference.
if (null === $this->destinationCurrency) {
$message = sprintf(
'Account #%d ("%s") must have currency preference but has none.',
$this->destinationAccount->id,
$this->destinationAccount->name
);
Log::error($message);
$this->friendlyError($message);
return true;
}
return false;
}
/**
* The source transaction must have a currency. If not, it will be added by
* taking it from the source account's preference.
*/
private function fixSourceNoCurrency(): void
{
if (null === $this->sourceTransaction->transaction_currency_id && null !== $this->sourceCurrency) {
$this->sourceTransaction
->transaction_currency_id
= (int)$this->sourceCurrency->id;
$message = sprintf(
'Transaction #%d has no currency setting, now set to %s.',
$this->sourceTransaction->id,
$this->sourceCurrency->code
);
$this->friendlyInfo($message);
$this->count++;
$this->sourceTransaction->save();
}
}
/**
* The source transaction must have the correct currency. If not, it will be set by
* taking it from the source account's preference.
*/
private function fixSourceUnmatchedCurrency(): void
{
if (null !== $this->sourceCurrency
&& null === $this->sourceTransaction->foreign_amount
&& (int)$this->sourceTransaction->transaction_currency_id !== (int)$this->sourceCurrency->id
) {
$message = sprintf(
'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.',
$this->sourceTransaction->id,
$this->sourceTransaction->transaction_currency_id,
$this->sourceAccount->id,
$this->sourceTransaction->amount
);
$this->friendlyWarning($message);
$this->count++;
$this->sourceTransaction->transaction_currency_id = (int)$this->sourceCurrency->id;
$this->sourceTransaction->save();
}
}
/** /**
* The destination transaction must have a currency. If not, it will be added by * The destination transaction must have a currency. If not, it will be added by
* taking it from the destination account's preference. * taking it from the destination account's preference.
@@ -107,26 +405,6 @@ class TransferCurrenciesCorrections extends Command
} }
} }
/**
* If the foreign amount of the destination transaction is null, but that of the other isn't, use this piece of code
* to restore it.
*/
private function fixDestNullForeignAmount(): void
{
if (null === $this->destinationTransaction->foreign_amount && null !== $this->sourceTransaction->foreign_amount) {
$this->destinationTransaction->foreign_amount = bcmul((string)$this->sourceTransaction->foreign_amount, '-1');
$this->destinationTransaction->save();
$this->count++;
$this->friendlyInfo(
sprintf(
'Restored foreign amount of destination transaction #%d to %s',
$this->destinationTransaction->id,
$this->destinationTransaction->foreign_amount
)
);
}
}
/** /**
* The destination transaction must have the correct currency. If not, it will be set by * The destination transaction must have the correct currency. If not, it will be set by
* taking it from the destination account's preference. * taking it from the destination account's preference.
@@ -193,27 +471,6 @@ class TransferCurrenciesCorrections extends Command
} }
} }
/**
* The source transaction must have a currency. If not, it will be added by
* taking it from the source account's preference.
*/
private function fixSourceNoCurrency(): void
{
if (null === $this->sourceTransaction->transaction_currency_id && null !== $this->sourceCurrency) {
$this->sourceTransaction
->transaction_currency_id
= (int)$this->sourceCurrency->id;
$message = sprintf(
'Transaction #%d has no currency setting, now set to %s.',
$this->sourceTransaction->id,
$this->sourceCurrency->code
);
$this->friendlyInfo($message);
$this->count++;
$this->sourceTransaction->save();
}
}
/** /**
* If the foreign amount of the source transaction is null, but that of the other isn't, use this piece of code * If the foreign amount of the source transaction is null, but that of the other isn't, use this piece of code
* to restore it. * to restore it.
@@ -235,26 +492,22 @@ class TransferCurrenciesCorrections extends Command
} }
/** /**
* The source transaction must have the correct currency. If not, it will be set by * If the foreign amount of the destination transaction is null, but that of the other isn't, use this piece of code
* taking it from the source account's preference. * to restore it.
*/ */
private function fixSourceUnmatchedCurrency(): void private function fixDestNullForeignAmount(): void
{ {
if (null !== $this->sourceCurrency if (null === $this->destinationTransaction->foreign_amount && null !== $this->sourceTransaction->foreign_amount) {
&& null === $this->sourceTransaction->foreign_amount $this->destinationTransaction->foreign_amount = bcmul((string)$this->sourceTransaction->foreign_amount, '-1');
&& (int)$this->sourceTransaction->transaction_currency_id !== (int)$this->sourceCurrency->id $this->destinationTransaction->save();
) {
$message = sprintf(
'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.',
$this->sourceTransaction->id,
$this->sourceTransaction->transaction_currency_id,
$this->sourceAccount->id,
$this->sourceTransaction->amount
);
$this->friendlyWarning($message);
$this->count++; $this->count++;
$this->sourceTransaction->transaction_currency_id = (int)$this->sourceCurrency->id; $this->friendlyInfo(
$this->sourceTransaction->save(); sprintf(
'Restored foreign amount of destination transaction #%d to %s',
$this->destinationTransaction->id,
$this->destinationTransaction->foreign_amount
)
);
} }
} }
@@ -281,148 +534,6 @@ class TransferCurrenciesCorrections extends Command
} }
} }
/**
* @param Account $account
*
* @return TransactionCurrency|null
*/
private function getCurrency(Account $account): ?TransactionCurrency
{
$accountId = $account->id;
if (array_key_exists($accountId, $this->accountCurrencies) && 0 === $this->accountCurrencies[$accountId]) {
return null;
}
if (array_key_exists($accountId, $this->accountCurrencies) && $this->accountCurrencies[$accountId] instanceof TransactionCurrency) {
return $this->accountCurrencies[$accountId];
}
$currency = $this->accountRepos->getAccountCurrency($account);
if (null === $currency) {
$this->accountCurrencies[$accountId] = 0;
return null;
}
$this->accountCurrencies[$accountId] = $currency;
return $currency;
}
/**
* Extract destination transaction, destination account + destination account currency from the journal.
*
* @param TransactionJournal $journal
*
*/
private function getDestinationInformation(TransactionJournal $journal): void
{
$this->destinationTransaction = $this->getDestinationTransaction($journal);
$this->destinationAccount = $this->destinationTransaction?->account;
$this->destinationCurrency = null === $this->destinationAccount ? null : $this->getCurrency($this->destinationAccount);
}
/**
* @param TransactionJournal $transfer
*
* @return Transaction|null
*/
private function getDestinationTransaction(TransactionJournal $transfer): ?Transaction
{
return $transfer->transactions()->where('amount', '>', 0)->first();
}
/**
* Extract source transaction, source account + source account currency from the journal.
*
* @param TransactionJournal $journal
*
*/
private function getSourceInformation(TransactionJournal $journal): void
{
$this->sourceTransaction = $this->getSourceTransaction($journal);
$this->sourceAccount = $this->sourceTransaction?->account;
$this->sourceCurrency = null === $this->sourceAccount ? null : $this->getCurrency($this->sourceAccount);
}
/**
* @param TransactionJournal $transfer
*
* @return Transaction|null
*/
private function getSourceTransaction(TransactionJournal $transfer): ?Transaction
{
return $transfer->transactions()->where('amount', '<', 0)->first();
}
/**
* Is either the source or destination transaction NULL?
*
* @return bool
*/
private function isEmptyTransactions(): bool
{
return null === $this->sourceTransaction || null === $this->destinationTransaction
|| null === $this->sourceAccount
|| null === $this->destinationAccount;
}
/**
* @return bool
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function isExecuted(): bool
{
$configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false);
if (null !== $configVar) {
return (bool)$configVar->data;
}
return false;
}
/**
* @return bool
*/
private function isNoCurrencyPresent(): bool
{
// source account must have a currency preference.
if (null === $this->sourceCurrency) {
$message = sprintf('Account #%d ("%s") must have currency preference but has none.', $this->sourceAccount->id, $this->sourceAccount->name);
Log::error($message);
$this->friendlyError($message);
return true;
}
// destination account must have a currency preference.
if (null === $this->destinationCurrency) {
$message = sprintf(
'Account #%d ("%s") must have currency preference but has none.',
$this->destinationAccount->id,
$this->destinationAccount->name
);
Log::error($message);
$this->friendlyError($message);
return true;
}
return false;
}
/**
* Is this a split transaction journal?
*
* @param TransactionJournal $transfer
*
* @return bool
*/
private function isSplitJournal(TransactionJournal $transfer): bool
{
return $transfer->transactions->count() > 2;
}
/** /**
* *
*/ */
@@ -430,115 +541,4 @@ class TransferCurrenciesCorrections extends Command
{ {
app('fireflyconfig')->set(self::CONFIG_NAME, true); app('fireflyconfig')->set(self::CONFIG_NAME, true);
} }
/**
* Reset all the class fields for the current transfer.
*
*/
private function resetInformation(): void
{
$this->sourceTransaction = null;
$this->sourceAccount = null;
$this->sourceCurrency = null;
$this->destinationTransaction = null;
$this->destinationAccount = null;
$this->destinationCurrency = null;
}
/**
* This routine verifies that transfers have the correct currency settings for the accounts they are linked to.
* For transfers, this is can be a destructive routine since we FORCE them into a currency setting whether they
* like it or not. Previous routines MUST have set the currency setting for both accounts for this to work.
*
* Both source and destination must match the respective currency preference. So FF3 must verify ALL
* transactions.
*/
private function startUpdateRoutine(): void
{
$set = $this->cliRepos->getAllJournals([TransactionType::TRANSFER]);
/** @var TransactionJournal $journal */
foreach ($set as $journal) {
$this->updateTransferCurrency($journal);
}
}
/**
* Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
* executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
* be called from the handle method instead of using the constructor to initialize the command.
*
*/
private function stupidLaravel(): void
{
$this->count = 0;
$this->accountRepos = app(AccountRepositoryInterface::class);
$this->cliRepos = app(JournalCLIRepositoryInterface::class);
$this->accountCurrencies = [];
$this->resetInformation();
}
/**
* @param TransactionJournal $transfer
*/
private function updateTransferCurrency(TransactionJournal $transfer): void
{
$this->resetInformation();
if ($this->isSplitJournal($transfer)) {
$this->friendlyWarning(sprintf('Transaction journal #%d is a split journal. Cannot continue.', $transfer->id));
return;
}
$this->getSourceInformation($transfer);
$this->getDestinationInformation($transfer);
// unexpectedly, either one is null:
if ($this->isEmptyTransactions()) {
$this->friendlyError(sprintf('Source or destination information for transaction journal #%d is null. Cannot fix this one.', $transfer->id));
return;
}
// both accounts must have currency preference:
if ($this->isNoCurrencyPresent()) {
$this->friendlyError(
sprintf('Source or destination accounts for transaction journal #%d have no currency information. Cannot fix this one.', $transfer->id)
);
return;
}
// fix source transaction having no currency.
$this->fixSourceNoCurrency();
// fix source transaction having bad currency.
$this->fixSourceUnmatchedCurrency();
// fix destination transaction having no currency.
$this->fixDestNoCurrency();
// fix destination transaction having bad currency.
$this->fixDestinationUnmatchedCurrency();
// remove foreign currency information if not necessary.
$this->fixInvalidForeignCurrency();
// correct foreign currency info if necessary.
$this->fixMismatchedForeignCurrency();
// restore missing foreign currency amount.
$this->fixSourceNullForeignAmount();
$this->fixDestNullForeignAmount();
// fix journal itself:
$this->fixTransactionJournalCurrency($transfer);
}
} }

View File

@@ -34,7 +34,6 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Services\Internal\Support\CreditRecalculateService; use FireflyIII\Services\Internal\Support\CreditRecalculateService;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface; use Psr\Container\NotFoundExceptionInterface;
@@ -70,48 +69,6 @@ class UpgradeLiabilities extends Command
return 0; return 0;
} }
/**
* @param Account $account
* @param TransactionJournal $openingBalance
*/
private function correctOpeningBalance(Account $account, TransactionJournal $openingBalance): void
{
$source = $this->getSourceTransaction($openingBalance);
$destination = $this->getDestinationTransaction($openingBalance);
if (null === $source || null === $destination) {
return;
}
// source MUST be the liability.
if ((int)$destination->account_id === (int)$account->id) {
// so if not, switch things around:
$sourceAccountId = (int)$source->account_id;
$source->account_id = $destination->account_id;
$destination->account_id = $sourceAccountId;
$source->save();
$destination->save();
}
}
/**
* @param TransactionJournal $journal
*
* @return Transaction|null
*/
private function getDestinationTransaction(TransactionJournal $journal): ?Transaction
{
return $journal->transactions()->where('amount', '>', 0)->first();
}
/**
* @param TransactionJournal $journal
*
* @return Transaction|null
*/
private function getSourceTransaction(TransactionJournal $journal): ?Transaction
{
return $journal->transactions()->where('amount', '<', 0)->first();
}
/** /**
* @return bool * @return bool
* @throws ContainerExceptionInterface * @throws ContainerExceptionInterface
@@ -130,9 +87,13 @@ class UpgradeLiabilities extends Command
/** /**
* *
*/ */
private function markAsExecuted(): void private function upgradeLiabilities(): void
{ {
app('fireflyconfig')->set(self::CONFIG_NAME, true); $users = User::get();
/** @var User $user */
foreach ($users as $user) {
$this->upgradeForUser($user);
}
} }
/** /**
@@ -153,18 +114,6 @@ class UpgradeLiabilities extends Command
} }
} }
/**
*
*/
private function upgradeLiabilities(): void
{
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
$this->upgradeForUser($user);
}
}
/** /**
* @param Account $account * @param Account $account
*/ */
@@ -189,4 +138,54 @@ class UpgradeLiabilities extends Command
$factory->crud($account, 'liability_direction', 'debit'); $factory->crud($account, 'liability_direction', 'debit');
} }
} }
/**
* @param Account $account
* @param TransactionJournal $openingBalance
*/
private function correctOpeningBalance(Account $account, TransactionJournal $openingBalance): void
{
$source = $this->getSourceTransaction($openingBalance);
$destination = $this->getDestinationTransaction($openingBalance);
if (null === $source || null === $destination) {
return;
}
// source MUST be the liability.
if ((int)$destination->account_id === (int)$account->id) {
// so if not, switch things around:
$sourceAccountId = (int)$source->account_id;
$source->account_id = $destination->account_id;
$destination->account_id = $sourceAccountId;
$source->save();
$destination->save();
}
}
/**
* @param TransactionJournal $journal
*
* @return Transaction|null
*/
private function getSourceTransaction(TransactionJournal $journal): ?Transaction
{
return $journal->transactions()->where('amount', '<', 0)->first();
}
/**
* @param TransactionJournal $journal
*
* @return Transaction|null
*/
private function getDestinationTransaction(TransactionJournal $journal): ?Transaction
{
return $journal->transactions()->where('amount', '>', 0)->first();
}
/**
*
*/
private function markAsExecuted(): void
{
app('fireflyconfig')->set(self::CONFIG_NAME, true);
}
} }

View File

@@ -72,6 +72,104 @@ class UpgradeLiabilitiesEight extends Command
return 0; return 0;
} }
/**
* @return bool
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function isExecuted(): bool
{
$configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false);
if (null !== $configVar) {
return (bool)$configVar->data;
}
return false;
}
/**
*
*/
private function upgradeLiabilities(): void
{
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
$this->upgradeForUser($user);
}
}
/**
* @param User $user
*/
private function upgradeForUser(User $user): void
{
$accounts = $user->accounts()
->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id')
->whereIn('account_types.type', config('firefly.valid_liabilities'))
->get(['accounts.*']);
/** @var Account $account */
foreach ($accounts as $account) {
$this->upgradeLiability($account);
$service = app(CreditRecalculateService::class);
$service->setAccount($account);
$service->recalculate();
}
}
/**
* @param Account $account
*/
private function upgradeLiability(Account $account): void
{
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$repository->setUser($account->user);
$direction = $repository->getMetaValue($account, 'liability_direction');
if ('credit' === $direction && $this->hasBadOpening($account)) {
$this->deleteCreditTransaction($account);
$this->reverseOpeningBalance($account);
$this->friendlyInfo(sprintf('Corrected opening balance for liability #%d ("%s")', $account->id, $account->name));
}
if ('credit' === $direction) {
$count = $this->deleteTransactions($account);
if ($count > 0) {
$this->friendlyInfo(sprintf('Removed %d old format transaction(s) for liability #%d ("%s")', $count, $account->id, $account->name));
}
}
}
/**
* @param Account $account
*
* @return bool
*/
private function hasBadOpening(Account $account): bool
{
$openingBalanceType = TransactionType::whereType(TransactionType::OPENING_BALANCE)->first();
$liabilityType = TransactionType::whereType(TransactionType::LIABILITY_CREDIT)->first();
$openingJournal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->where('transactions.account_id', $account->id)
->where('transaction_journals.transaction_type_id', $openingBalanceType->id)
->first(['transaction_journals.*']);
if (null === $openingJournal) {
return false;
}
$liabilityJournal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->where('transactions.account_id', $account->id)
->where('transaction_journals.transaction_type_id', $liabilityType->id)
->first(['transaction_journals.*']);
if (null === $liabilityJournal) {
return false;
}
if (!$openingJournal->date->isSameDay($liabilityJournal->date)) {
return false;
}
return true;
}
/** /**
* @param Account $account * @param Account $account
* *
@@ -93,6 +191,36 @@ class UpgradeLiabilitiesEight extends Command
} }
} }
/**
* @param Account $account
*
* @return void
*/
private function reverseOpeningBalance(Account $account): void
{
$openingBalanceType = TransactionType::whereType(TransactionType::OPENING_BALANCE)->first();
/** @var TransactionJournal $openingJournal */
$openingJournal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->where('transactions.account_id', $account->id)
->where('transaction_journals.transaction_type_id', $openingBalanceType->id)
->first(['transaction_journals.*']);
/** @var Transaction|null $source */
$source = $openingJournal->transactions()->where('amount', '<', 0)->first();
/** @var Transaction|null $dest */
$dest = $openingJournal->transactions()->where('amount', '>', 0)->first();
if ($source && $dest) {
$sourceId = $source->account_id;
$destId = $dest->account_id;
$dest->account_id = $sourceId;
$source->account_id = $destId;
$source->save();
$dest->save();
return;
}
Log::warning('Did not find opening balance.');
}
/** /**
* @param $account * @param $account
* *
@@ -134,51 +262,6 @@ class UpgradeLiabilitiesEight extends Command
return $count; return $count;
} }
/**
* @param Account $account
*
* @return bool
*/
private function hasBadOpening(Account $account): bool
{
$openingBalanceType = TransactionType::whereType(TransactionType::OPENING_BALANCE)->first();
$liabilityType = TransactionType::whereType(TransactionType::LIABILITY_CREDIT)->first();
$openingJournal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->where('transactions.account_id', $account->id)
->where('transaction_journals.transaction_type_id', $openingBalanceType->id)
->first(['transaction_journals.*']);
if (null === $openingJournal) {
return false;
}
$liabilityJournal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->where('transactions.account_id', $account->id)
->where('transaction_journals.transaction_type_id', $liabilityType->id)
->first(['transaction_journals.*']);
if (null === $liabilityJournal) {
return false;
}
if (!$openingJournal->date->isSameDay($liabilityJournal->date)) {
return false;
}
return true;
}
/**
* @return bool
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function isExecuted(): bool
{
$configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false);
if (null !== $configVar) {
return (bool)$configVar->data;
}
return false;
}
/** /**
* *
*/ */
@@ -186,87 +269,4 @@ class UpgradeLiabilitiesEight extends Command
{ {
app('fireflyconfig')->set(self::CONFIG_NAME, true); app('fireflyconfig')->set(self::CONFIG_NAME, true);
} }
/**
* @param Account $account
*
* @return void
*/
private function reverseOpeningBalance(Account $account): void
{
$openingBalanceType = TransactionType::whereType(TransactionType::OPENING_BALANCE)->first();
/** @var TransactionJournal $openingJournal */
$openingJournal = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->where('transactions.account_id', $account->id)
->where('transaction_journals.transaction_type_id', $openingBalanceType->id)
->first(['transaction_journals.*']);
/** @var Transaction|null $source */
$source = $openingJournal->transactions()->where('amount', '<', 0)->first();
/** @var Transaction|null $dest */
$dest = $openingJournal->transactions()->where('amount', '>', 0)->first();
if ($source && $dest) {
$sourceId = $source->account_id;
$destId = $dest->account_id;
$dest->account_id = $sourceId;
$source->account_id = $destId;
$source->save();
$dest->save();
return;
}
Log::warning('Did not find opening balance.');
}
/**
* @param User $user
*/
private function upgradeForUser(User $user): void
{
$accounts = $user->accounts()
->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id')
->whereIn('account_types.type', config('firefly.valid_liabilities'))
->get(['accounts.*']);
/** @var Account $account */
foreach ($accounts as $account) {
$this->upgradeLiability($account);
$service = app(CreditRecalculateService::class);
$service->setAccount($account);
$service->recalculate();
}
}
/**
*
*/
private function upgradeLiabilities(): void
{
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
$this->upgradeForUser($user);
}
}
/**
* @param Account $account
*/
private function upgradeLiability(Account $account): void
{
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$repository->setUser($account->user);
$direction = $repository->getMetaValue($account, 'liability_direction');
if ('credit' === $direction && $this->hasBadOpening($account)) {
$this->deleteCreditTransaction($account);
$this->reverseOpeningBalance($account);
$this->friendlyInfo(sprintf('Corrected opening balance for liability #%d ("%s")', $account->id, $account->name));
}
if ('credit' === $direction) {
$count = $this->deleteTransactions($account);
if ($count > 0) {
$this->friendlyInfo(sprintf('Removed %d old format transaction(s) for liability #%d ("%s")', $count, $account->id, $account->name));
}
}
}
} }

View File

@@ -40,6 +40,7 @@ class RegisteredUser extends Event
/** /**
* Create a new event instance. This event is triggered when a new user registers. * Create a new event instance. This event is triggered when a new user registers.
*
* @param User $user * @param User $user
*/ */
public function __construct(User $user) public function __construct(User $user)

View File

@@ -168,6 +168,45 @@ class GracefulNotFoundHandler extends ExceptionHandler
return redirect(route('accounts.index', [$shortType])); return redirect(route('accounts.index', [$shortType]));
} }
/**
* @param Request $request
* @param Throwable $exception
*
* @return Response
* @throws Throwable
*/
private function handleGroup(Request $request, Throwable $exception)
{
Log::debug('404 page is probably a deleted group. Redirect to overview of group types.');
/** @var User $user */
$user = auth()->user();
$route = $request->route();
$groupId = (int)$route->parameter('transactionGroup');
/** @var TransactionGroup|null $group */
$group = $user->transactionGroups()->withTrashed()->find($groupId);
if (null === $group) {
Log::error(sprintf('Could not find group %d, so give big fat error.', $groupId));
return parent::render($request, $exception);
}
/** @var TransactionJournal|null $journal */
$journal = $group->transactionJournals()->withTrashed()->first();
if (null === $journal) {
Log::error(sprintf('Could not find journal for group %d, so give big fat error.', $groupId));
return parent::render($request, $exception);
}
$type = $journal->transactionType->type;
$request->session()->reflash();
if (TransactionType::RECONCILIATION === $type) {
return redirect(route('accounts.index', ['asset']));
}
return redirect(route('transactions.index', [strtolower($type)]));
}
/** /**
* @param Request $request * @param Request $request
* @param Throwable $exception * @param Throwable $exception
@@ -211,43 +250,4 @@ class GracefulNotFoundHandler extends ExceptionHandler
return parent::render($request, $exception); return parent::render($request, $exception);
} }
/**
* @param Request $request
* @param Throwable $exception
*
* @return Response
* @throws Throwable
*/
private function handleGroup(Request $request, Throwable $exception)
{
Log::debug('404 page is probably a deleted group. Redirect to overview of group types.');
/** @var User $user */
$user = auth()->user();
$route = $request->route();
$groupId = (int)$route->parameter('transactionGroup');
/** @var TransactionGroup|null $group */
$group = $user->transactionGroups()->withTrashed()->find($groupId);
if (null === $group) {
Log::error(sprintf('Could not find group %d, so give big fat error.', $groupId));
return parent::render($request, $exception);
}
/** @var TransactionJournal|null $journal */
$journal = $group->transactionJournals()->withTrashed()->first();
if (null === $journal) {
Log::error(sprintf('Could not find journal for group %d, so give big fat error.', $groupId));
return parent::render($request, $exception);
}
$type = $journal->transactionType->type;
$request->session()->reflash();
if (TransactionType::RECONCILIATION === $type) {
return redirect(route('accounts.index', ['asset']));
}
return redirect(route('transactions.index', [strtolower($type)]));
}
} }

View File

@@ -225,6 +225,23 @@ class Handler extends ExceptionHandler
parent::report($e); parent::report($e);
} }
/**
* @param Throwable $e
*
* @return bool
*/
private function shouldntReportLocal(Throwable $e): bool
{
return !is_null(
Arr::first(
$this->dontReport,
function ($type) use ($e) {
return $e instanceof $type;
}
)
);
}
/** /**
* Convert a validation exception into a response. * Convert a validation exception into a response.
* *
@@ -263,21 +280,4 @@ class Handler extends ExceptionHandler
return null !== $previousHost && $previousHost === $safeHost ? $previous : $safe; return null !== $previousHost && $previousHost === $safeHost ? $previous : $safe;
} }
/**
* @param Throwable $e
*
* @return bool
*/
private function shouldntReportLocal(Throwable $e): bool
{
return !is_null(
Arr::first(
$this->dontReport,
function ($type) use ($e) {
return $e instanceof $type;
}
)
);
}
} }

View File

@@ -69,48 +69,6 @@ class AccountFactory
$this->validFields = config('firefly.valid_account_fields'); $this->validFields = config('firefly.valid_account_fields');
} }
/**
* @param array $data
*
* @return Account
* @throws FireflyException
* @throws JsonException
*/
public function create(array $data): Account
{
Log::debug('Now in AccountFactory::create()');
$type = $this->getAccountType($data);
$data['iban'] = $this->filterIban($data['iban'] ?? null);
// account may exist already:
$return = $this->find($data['name'], $type->type);
if (null !== $return) {
return $return;
}
$return = $this->createAccount($type, $data);
event(new StoredAccount($return));
return $return;
}
/**
* @param string $accountName
* @param string $accountType
*
* @return Account|null
*/
public function find(string $accountName, string $accountType): ?Account
{
Log::debug(sprintf('Now in AccountFactory::find("%s", "%s")', $accountName, $accountType));
$type = AccountType::whereType($accountType)->first();
/** @var Account|null */
return $this->user->accounts()->where('account_type_id', $type->id)->where('name', $accountName)->first();
}
/** /**
* @param string $accountName * @param string $accountName
* @param string $accountType * @param string $accountType
@@ -148,12 +106,30 @@ class AccountFactory
} }
/** /**
* @param User $user * @param array $data
*
* @return Account
* @throws FireflyException
* @throws JsonException
*/ */
public function setUser(User $user): void public function create(array $data): Account
{ {
$this->user = $user; Log::debug('Now in AccountFactory::create()');
$this->accountRepository->setUser($user); $type = $this->getAccountType($data);
$data['iban'] = $this->filterIban($data['iban'] ?? null);
// account may exist already:
$return = $this->find($data['name'], $type->type);
if (null !== $return) {
return $return;
}
$return = $this->createAccount($type, $data);
event(new StoredAccount($return));
return $return;
} }
/** /**
@@ -192,32 +168,18 @@ class AccountFactory
} }
/** /**
* @param Account $account * @param string $accountName
* @param array $data * @param string $accountType
* *
* @return array * @return Account|null
* @throws FireflyException
* @throws JsonException
*/ */
private function cleanMetaDataArray(Account $account, array $data): array public function find(string $accountName, string $accountType): ?Account
{ {
$currencyId = array_key_exists('currency_id', $data) ? (int)$data['currency_id'] : 0; Log::debug(sprintf('Now in AccountFactory::find("%s", "%s")', $accountName, $accountType));
$currencyCode = array_key_exists('currency_code', $data) ? (string)$data['currency_code'] : ''; $type = AccountType::whereType($accountType)->first();
$accountRole = array_key_exists('account_role', $data) ? (string)$data['account_role'] : null;
$currency = $this->getCurrency($currencyId, $currencyCode);
// only asset account may have a role: /** @var Account|null */
if ($account->accountType->type !== AccountType::ASSET) { return $this->user->accounts()->where('account_type_id', $type->id)->where('name', $accountName)->first();
$accountRole = '';
}
// only liability may have direction:
if (array_key_exists('liability_direction', $data) && !in_array($account->accountType->type, config('firefly.valid_liabilities'), true)) {
$data['liability_direction'] = null;
}
$data['account_role'] = $accountRole;
$data['currency_id'] = $currency->id;
return $data;
} }
/** /**
@@ -293,29 +255,29 @@ class AccountFactory
* @param Account $account * @param Account $account
* @param array $data * @param array $data
* *
* @return array
* @throws FireflyException * @throws FireflyException
* @throws JsonException
*/ */
private function storeCreditLiability(Account $account, array $data): void private function cleanMetaDataArray(Account $account, array $data): array
{ {
Log::debug('storeCreditLiability'); $currencyId = array_key_exists('currency_id', $data) ? (int)$data['currency_id'] : 0;
$account->refresh(); $currencyCode = array_key_exists('currency_code', $data) ? (string)$data['currency_code'] : '';
$accountType = $account->accountType->type; $accountRole = array_key_exists('account_role', $data) ? (string)$data['account_role'] : null;
$direction = $this->accountRepository->getMetaValue($account, 'liability_direction'); $currency = $this->getCurrency($currencyId, $currencyCode);
$valid = config('firefly.valid_liabilities');
if (in_array($accountType, $valid, true)) { // only asset account may have a role:
Log::debug('Is a liability with credit ("i am owed") direction.'); if ($account->accountType->type !== AccountType::ASSET) {
if ($this->validOBData($data)) { $accountRole = '';
Log::debug('Has valid CL data.');
$openingBalance = $data['opening_balance'];
$openingBalanceDate = $data['opening_balance_date'];
// store credit transaction.
$this->updateCreditTransaction($account, $direction, $openingBalance, $openingBalanceDate);
}
if (!$this->validOBData($data)) {
Log::debug('Does NOT have valid CL data, deletr any CL transaction.');
$this->deleteCreditTransaction($account);
} }
// only liability may have direction:
if (array_key_exists('liability_direction', $data) && !in_array($account->accountType->type, config('firefly.valid_liabilities'), true)) {
$data['liability_direction'] = null;
} }
$data['account_role'] = $accountRole;
$data['currency_id'] = $currency->id;
return $data;
} }
/** /**
@@ -383,6 +345,35 @@ class AccountFactory
} }
} }
/**
* @param Account $account
* @param array $data
*
* @throws FireflyException
*/
private function storeCreditLiability(Account $account, array $data): void
{
Log::debug('storeCreditLiability');
$account->refresh();
$accountType = $account->accountType->type;
$direction = $this->accountRepository->getMetaValue($account, 'liability_direction');
$valid = config('firefly.valid_liabilities');
if (in_array($accountType, $valid, true)) {
Log::debug('Is a liability with credit ("i am owed") direction.');
if ($this->validOBData($data)) {
Log::debug('Has valid CL data.');
$openingBalance = $data['opening_balance'];
$openingBalanceDate = $data['opening_balance_date'];
// store credit transaction.
$this->updateCreditTransaction($account, $direction, $openingBalance, $openingBalanceDate);
}
if (!$this->validOBData($data)) {
Log::debug('Does NOT have valid CL data, deletr any CL transaction.');
$this->deleteCreditTransaction($account);
}
}
}
/** /**
* @param Account $account * @param Account $account
* @param array $data * @param array $data
@@ -406,4 +397,13 @@ class AccountFactory
$updateService->setUser($account->user); $updateService->setUser($account->user);
$updateService->update($account, ['order' => $order]); $updateService->update($account, ['order' => $order]);
} }
/**
* @param User $user
*/
public function setUser(User $user): void
{
$this->user = $user;
$this->accountRepository->setUser($user);
}
} }

View File

@@ -26,23 +26,12 @@ namespace FireflyIII\Factory;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Models\AccountMeta; use FireflyIII\Models\AccountMeta;
use Illuminate\Support\Facades\Log;
/** /**
* Class AccountMetaFactory * Class AccountMetaFactory
*/ */
class AccountMetaFactory class AccountMetaFactory
{ {
/**
* @param array $data
*
* @return AccountMeta|null
*/
public function create(array $data): ?AccountMeta
{
return AccountMeta::create($data);
}
/** /**
* Create update or delete meta data. * Create update or delete meta data.
* *
@@ -75,4 +64,14 @@ class AccountMetaFactory
return $entry; return $entry;
} }
/**
* @param array $data
*
* @return AccountMeta|null
*/
public function create(array $data): ?AccountMeta
{
return AccountMeta::create($data);
}
} }

View File

@@ -36,16 +36,6 @@ class CategoryFactory
{ {
private User $user; private User $user;
/**
* @param string $name
*
* @return Category|null
*/
public function findByName(string $name): ?Category
{
return $this->user->categories()->where('name', $name)->first();
}
/** /**
* @param int|null $categoryId * @param int|null $categoryId
* @param null|string $categoryName * @param null|string $categoryName
@@ -93,6 +83,16 @@ class CategoryFactory
return null; return null;
} }
/**
* @param string $name
*
* @return Category|null
*/
public function findByName(string $name): ?Category
{
return $this->user->categories()->where('name', $name)->first();
}
/** /**
* @param User $user * @param User $user
*/ */

View File

@@ -35,40 +35,6 @@ class TagFactory
{ {
private User $user; private User $user;
/**
* @param array $data
*
* @return Tag|null
*/
public function create(array $data): ?Tag
{
$zoomLevel = 0 === (int)$data['zoom_level'] ? null : (int)$data['zoom_level'];
$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,
];
$tag = Tag::create($array);
if (null !== $tag && null !== $latitude && null !== $longitude) {
// create location object.
$location = new Location();
$location->latitude = $latitude;
$location->longitude = $longitude;
$location->zoom_level = $zoomLevel;
$location->locatable()->associate($tag);
$location->save();
}
return $tag;
}
/** /**
* @param string $tag * @param string $tag
* *
@@ -106,6 +72,40 @@ class TagFactory
return $newTag; return $newTag;
} }
/**
* @param array $data
*
* @return Tag|null
*/
public function create(array $data): ?Tag
{
$zoomLevel = 0 === (int)$data['zoom_level'] ? null : (int)$data['zoom_level'];
$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,
];
$tag = Tag::create($array);
if (null !== $tag && null !== $latitude && null !== $longitude) {
// create location object.
$location = new Location();
$location->latitude = $latitude;
$location->longitude = $longitude;
$location->zoom_level = $zoomLevel;
$location->locatable()->associate($tag);
$location->save();
}
return $tag;
}
/** /**
* @param User $user * @param User $user
*/ */

View File

@@ -80,95 +80,6 @@ class TransactionFactory
return $this->create(app('steam')->negative($amount), $foreignAmount); return $this->create(app('steam')->negative($amount), $foreignAmount);
} }
/**
* Create transaction with positive amount (for destination accounts).
*
* @param string $amount
* @param string|null $foreignAmount
*
* @return Transaction
* @throws FireflyException
*/
public function createPositive(string $amount, ?string $foreignAmount): Transaction
{
if ('' === $foreignAmount) {
$foreignAmount = null;
}
if (null !== $foreignAmount) {
$foreignAmount = app('steam')->positive($foreignAmount);
}
return $this->create(app('steam')->positive($amount), $foreignAmount);
}
/**
* @param Account $account
*
*/
public function setAccount(Account $account): void
{
$this->account = $account;
}
/**
* @param array $accountInformation
*/
public function setAccountInformation(array $accountInformation): void
{
$this->accountInformation = $accountInformation;
}
/**
* @param TransactionCurrency $currency
*
*/
public function setCurrency(TransactionCurrency $currency): void
{
$this->currency = $currency;
}
/**
* @param TransactionCurrency|null $foreignCurrency |null
*
*/
public function setForeignCurrency(?TransactionCurrency $foreignCurrency): void
{
$this->foreignCurrency = $foreignCurrency;
}
/**
* @param TransactionJournal $journal
*
*/
public function setJournal(TransactionJournal $journal): void
{
$this->journal = $journal;
}
/**
* @param bool $reconciled
*
*/
public function setReconciled(bool $reconciled): void
{
$this->reconciled = $reconciled;
}
/**
* @param User $user
*
*/
public function setUser(User $user): void
{
// empty function.
}
/** /**
* @param string $amount * @param string $amount
* @param string|null $foreignAmount * @param string|null $foreignAmount
@@ -260,4 +171,93 @@ class TransactionFactory
$service = app(AccountUpdateService::class); $service = app(AccountUpdateService::class);
$service->update($this->account, ['iban' => $this->accountInformation['iban']]); $service->update($this->account, ['iban' => $this->accountInformation['iban']]);
} }
/**
* Create transaction with positive amount (for destination accounts).
*
* @param string $amount
* @param string|null $foreignAmount
*
* @return Transaction
* @throws FireflyException
*/
public function createPositive(string $amount, ?string $foreignAmount): Transaction
{
if ('' === $foreignAmount) {
$foreignAmount = null;
}
if (null !== $foreignAmount) {
$foreignAmount = app('steam')->positive($foreignAmount);
}
return $this->create(app('steam')->positive($amount), $foreignAmount);
}
/**
* @param Account $account
*
*/
public function setAccount(Account $account): void
{
$this->account = $account;
}
/**
* @param array $accountInformation
*/
public function setAccountInformation(array $accountInformation): void
{
$this->accountInformation = $accountInformation;
}
/**
* @param TransactionCurrency $currency
*
*/
public function setCurrency(TransactionCurrency $currency): void
{
$this->currency = $currency;
}
/**
* @param TransactionCurrency|null $foreignCurrency |null
*
*/
public function setForeignCurrency(?TransactionCurrency $foreignCurrency): void
{
$this->foreignCurrency = $foreignCurrency;
}
/**
* @param TransactionJournal $journal
*
*/
public function setJournal(TransactionJournal $journal): void
{
$this->journal = $journal;
}
/**
* @param bool $reconciled
*
*/
public function setReconciled(bool $reconciled): void
{
$this->reconciled = $reconciled;
}
/**
* @param User $user
*
*/
public function setUser(User $user): void
{
// empty function.
}
} }

View File

@@ -143,79 +143,6 @@ class TransactionJournalFactory
return $collection; return $collection;
} }
/**
* @param bool $errorOnHash
*/
public function setErrorOnHash(bool $errorOnHash): void
{
$this->errorOnHash = $errorOnHash;
if (true === $errorOnHash) {
Log::info('Will trigger duplication alert for this journal.');
}
}
/**
* Set the user.
*
* @param User $user
*/
public function setUser(User $user): void
{
$this->user = $user;
$this->currencyRepository->setUser($this->user);
$this->tagFactory->setUser($user);
$this->billRepository->setUser($this->user);
$this->budgetRepository->setUser($this->user);
$this->categoryRepository->setUser($this->user);
$this->piggyRepository->setUser($this->user);
$this->accountRepository->setUser($this->user);
}
/**
* @param TransactionJournal $journal
* @param NullArrayObject $data
* @param string $field
*/
protected function storeMeta(TransactionJournal $journal, NullArrayObject $data, string $field): void
{
$set = [
'journal' => $journal,
'name' => $field,
'data' => (string)($data[$field] ?? ''),
];
if ($data[$field] instanceof Carbon) {
$data[$field]->setTimezone(config('app.timezone'));
Log::debug(sprintf('%s Date: %s (%s)', $field, $data[$field], $data[$field]->timezone->getName()));
$set['data'] = $data[$field]->format('Y-m-d H:i:s');
}
Log::debug(sprintf('Going to store meta-field "%s", with value "%s".', $set['name'], $set['data']));
/** @var TransactionJournalMetaFactory $factory */
$factory = app(TransactionJournalMetaFactory::class);
$factory->updateOrCreate($set);
}
/**
* Set foreign currency to NULL if it's the same as the normal currency:
*
* @param TransactionCurrency|null $currency
* @param TransactionCurrency|null $foreignCurrency
*
* @return TransactionCurrency|null
*/
private function compareCurrencies(?TransactionCurrency $currency, ?TransactionCurrency $foreignCurrency): ?TransactionCurrency
{
if (null === $currency) {
return null;
}
if (null !== $foreignCurrency && $foreignCurrency->id === $currency->id) {
return null;
}
return $foreignCurrency;
}
/** /**
* @param NullArrayObject $row * @param NullArrayObject $row
* *
@@ -377,6 +304,24 @@ class TransactionJournalFactory
return $journal; return $journal;
} }
/**
* @param NullArrayObject $row
*
* @return string
* @throws JsonException
*/
private function hashArray(NullArrayObject $row): string
{
$dataRow = $row->getArrayCopy();
unset($dataRow['import_hash_v2'], $dataRow['original_source']);
$json = json_encode($dataRow, JSON_THROW_ON_ERROR);
$hash = hash('sha256', $json);
Log::debug(sprintf('The hash is: %s', $hash), $dataRow);
return $hash;
}
/** /**
* If this transaction already exists, throw an error. * If this transaction already exists, throw an error.
* *
@@ -409,183 +354,6 @@ class TransactionJournalFactory
} }
} }
/**
* Force the deletion of an entire set of transaction journals and their meta object in case of
* an error creating a group.
*
* @param Collection $collection
*/
private function forceDeleteOnError(Collection $collection): void
{
Log::debug(sprintf('forceDeleteOnError on collection size %d item(s)', $collection->count()));
$service = app(JournalDestroyService::class);
/** @var TransactionJournal $journal */
foreach ($collection as $journal) {
Log::debug(sprintf('forceDeleteOnError on journal #%d', $journal->id));
$service->destroy($journal);
}
}
/**
* @param Transaction $transaction
*/
private function forceTrDelete(Transaction $transaction): void
{
$transaction->delete();
}
/**
* @param TransactionCurrency|null $currency
* @param Account $account
*
* @return TransactionCurrency
* @throws FireflyException
* @throws JsonException
*/
private function getCurrency(?TransactionCurrency $currency, Account $account): TransactionCurrency
{
Log::debug('Now in getCurrency()');
/** @var Preference|null $preference */
$preference = $this->accountRepository->getAccountCurrency($account);
if (null === $preference && null === $currency) {
// return user's default:
return app('amount')->getDefaultCurrencyByUser($this->user);
}
$result = ($preference ?? $currency) ?? app('amount')->getSystemCurrency();
Log::debug(sprintf('Currency is now #%d (%s) because of account #%d (%s)', $result->id, $result->code, $account->id, $account->name));
return $result;
}
/**
* @param string $type
* @param TransactionCurrency|null $currency
* @param Account $source
* @param Account $destination
*
* @return TransactionCurrency
* @throws FireflyException
* @throws JsonException
*/
private function getCurrencyByAccount(string $type, ?TransactionCurrency $currency, Account $source, Account $destination): TransactionCurrency
{
Log::debug('Now in getCurrencyByAccount()');
return match ($type) {
default => $this->getCurrency($currency, $source),
TransactionType::DEPOSIT => $this->getCurrency($currency, $destination),
};
}
/**
* @param string $description
*
* @return string
*/
private function getDescription(string $description): string
{
$description = '' === $description ? '(empty description)' : $description;
return substr($description, 0, 255);
}
/**
* @param string $type
* @param TransactionCurrency|null $foreignCurrency
* @param Account $destination
*
* @return TransactionCurrency|null
* @throws FireflyException
* @throws JsonException
*/
private function getForeignByAccount(string $type, ?TransactionCurrency $foreignCurrency, Account $destination): ?TransactionCurrency
{
if (TransactionType::TRANSFER === $type) {
return $this->getCurrency($foreignCurrency, $destination);
}
return $foreignCurrency;
}
/**
* @param NullArrayObject $row
*
* @return string
* @throws JsonException
*/
private function hashArray(NullArrayObject $row): string
{
$dataRow = $row->getArrayCopy();
unset($dataRow['import_hash_v2'], $dataRow['original_source']);
$json = json_encode($dataRow, JSON_THROW_ON_ERROR);
$hash = hash('sha256', $json);
Log::debug(sprintf('The hash is: %s', $hash), $dataRow);
return $hash;
}
/**
* @param Account|null $sourceAccount
* @param Account|null $destinationAccount
* @return array
*/
private function reconciliationSanityCheck(?Account $sourceAccount, ?Account $destinationAccount): array
{
Log::debug(sprintf('Now in %s', __METHOD__));
if (null !== $sourceAccount && null !== $destinationAccount) {
Log::debug('Both accounts exist, simply return them.');
return [$sourceAccount, $destinationAccount];
}
if (null !== $sourceAccount && null === $destinationAccount) {
Log::debug('Destination account is NULL, source account is not.');
$account = $this->accountRepository->getReconciliation($sourceAccount);
Log::debug(sprintf('Will return account #%d ("%s") of type "%s"', $account->id, $account->name, $account->accountType->type));
return [$sourceAccount, $account];
}
if (null === $sourceAccount && null !== $destinationAccount) {
Log::debug('Source account is NULL, destination account is not.');
$account = $this->accountRepository->getReconciliation($destinationAccount);
Log::debug(sprintf('Will return account #%d ("%s") of type "%s"', $account->id, $account->name, $account->accountType->type));
return [$account, $destinationAccount];
}
Log::debug('Unused fallback');
return [$sourceAccount, $destinationAccount];
}
/**
* @param TransactionJournal $journal
* @param NullArrayObject $transaction
*/
private function storeMetaFields(TransactionJournal $journal, NullArrayObject $transaction): void
{
foreach ($this->fields as $field) {
$this->storeMeta($journal, $transaction, $field);
}
}
/**
* Link a piggy bank to this journal.
*
* @param TransactionJournal $journal
* @param NullArrayObject $data
*/
private function storePiggyEvent(TransactionJournal $journal, NullArrayObject $data): void
{
Log::debug('Will now store piggy event.');
$piggyBank = $this->piggyRepository->findPiggyBank((int)$data['piggy_bank_id'], $data['piggy_bank_name']);
if (null !== $piggyBank) {
$this->piggyEventFactory->create($journal, $piggyBank);
Log::debug('Create piggy event.');
return;
}
Log::debug('Create no piggy event');
}
/** /**
* @param NullArrayObject $data * @param NullArrayObject $data
* *
@@ -627,4 +395,236 @@ class TransactionJournalFactory
throw new FireflyException(sprintf('Destination: %s', $this->accountValidator->destError)); throw new FireflyException(sprintf('Destination: %s', $this->accountValidator->destError));
} }
} }
/**
* Set the user.
*
* @param User $user
*/
public function setUser(User $user): void
{
$this->user = $user;
$this->currencyRepository->setUser($this->user);
$this->tagFactory->setUser($user);
$this->billRepository->setUser($this->user);
$this->budgetRepository->setUser($this->user);
$this->categoryRepository->setUser($this->user);
$this->piggyRepository->setUser($this->user);
$this->accountRepository->setUser($this->user);
}
/**
* @param Account|null $sourceAccount
* @param Account|null $destinationAccount
* @return array
*/
private function reconciliationSanityCheck(?Account $sourceAccount, ?Account $destinationAccount): array
{
Log::debug(sprintf('Now in %s', __METHOD__));
if (null !== $sourceAccount && null !== $destinationAccount) {
Log::debug('Both accounts exist, simply return them.');
return [$sourceAccount, $destinationAccount];
}
if (null !== $sourceAccount && null === $destinationAccount) {
Log::debug('Destination account is NULL, source account is not.');
$account = $this->accountRepository->getReconciliation($sourceAccount);
Log::debug(sprintf('Will return account #%d ("%s") of type "%s"', $account->id, $account->name, $account->accountType->type));
return [$sourceAccount, $account];
}
if (null === $sourceAccount && null !== $destinationAccount) {
Log::debug('Source account is NULL, destination account is not.');
$account = $this->accountRepository->getReconciliation($destinationAccount);
Log::debug(sprintf('Will return account #%d ("%s") of type "%s"', $account->id, $account->name, $account->accountType->type));
return [$account, $destinationAccount];
}
Log::debug('Unused fallback');
return [$sourceAccount, $destinationAccount];
}
/**
* @param string $type
* @param TransactionCurrency|null $currency
* @param Account $source
* @param Account $destination
*
* @return TransactionCurrency
* @throws FireflyException
* @throws JsonException
*/
private function getCurrencyByAccount(string $type, ?TransactionCurrency $currency, Account $source, Account $destination): TransactionCurrency
{
Log::debug('Now in getCurrencyByAccount()');
return match ($type) {
default => $this->getCurrency($currency, $source),
TransactionType::DEPOSIT => $this->getCurrency($currency, $destination),
};
}
/**
* @param TransactionCurrency|null $currency
* @param Account $account
*
* @return TransactionCurrency
* @throws FireflyException
* @throws JsonException
*/
private function getCurrency(?TransactionCurrency $currency, Account $account): TransactionCurrency
{
Log::debug('Now in getCurrency()');
/** @var Preference|null $preference */
$preference = $this->accountRepository->getAccountCurrency($account);
if (null === $preference && null === $currency) {
// return user's default:
return app('amount')->getDefaultCurrencyByUser($this->user);
}
$result = ($preference ?? $currency) ?? app('amount')->getSystemCurrency();
Log::debug(sprintf('Currency is now #%d (%s) because of account #%d (%s)', $result->id, $result->code, $account->id, $account->name));
return $result;
}
/**
* Set foreign currency to NULL if it's the same as the normal currency:
*
* @param TransactionCurrency|null $currency
* @param TransactionCurrency|null $foreignCurrency
*
* @return TransactionCurrency|null
*/
private function compareCurrencies(?TransactionCurrency $currency, ?TransactionCurrency $foreignCurrency): ?TransactionCurrency
{
if (null === $currency) {
return null;
}
if (null !== $foreignCurrency && $foreignCurrency->id === $currency->id) {
return null;
}
return $foreignCurrency;
}
/**
* @param string $type
* @param TransactionCurrency|null $foreignCurrency
* @param Account $destination
*
* @return TransactionCurrency|null
* @throws FireflyException
* @throws JsonException
*/
private function getForeignByAccount(string $type, ?TransactionCurrency $foreignCurrency, Account $destination): ?TransactionCurrency
{
if (TransactionType::TRANSFER === $type) {
return $this->getCurrency($foreignCurrency, $destination);
}
return $foreignCurrency;
}
/**
* @param string $description
*
* @return string
*/
private function getDescription(string $description): string
{
$description = '' === $description ? '(empty description)' : $description;
return substr($description, 0, 255);
}
/**
* Force the deletion of an entire set of transaction journals and their meta object in case of
* an error creating a group.
*
* @param Collection $collection
*/
private function forceDeleteOnError(Collection $collection): void
{
Log::debug(sprintf('forceDeleteOnError on collection size %d item(s)', $collection->count()));
$service = app(JournalDestroyService::class);
/** @var TransactionJournal $journal */
foreach ($collection as $journal) {
Log::debug(sprintf('forceDeleteOnError on journal #%d', $journal->id));
$service->destroy($journal);
}
}
/**
* @param Transaction $transaction
*/
private function forceTrDelete(Transaction $transaction): void
{
$transaction->delete();
}
/**
* Link a piggy bank to this journal.
*
* @param TransactionJournal $journal
* @param NullArrayObject $data
*/
private function storePiggyEvent(TransactionJournal $journal, NullArrayObject $data): void
{
Log::debug('Will now store piggy event.');
$piggyBank = $this->piggyRepository->findPiggyBank((int)$data['piggy_bank_id'], $data['piggy_bank_name']);
if (null !== $piggyBank) {
$this->piggyEventFactory->create($journal, $piggyBank);
Log::debug('Create piggy event.');
return;
}
Log::debug('Create no piggy event');
}
/**
* @param TransactionJournal $journal
* @param NullArrayObject $transaction
*/
private function storeMetaFields(TransactionJournal $journal, NullArrayObject $transaction): void
{
foreach ($this->fields as $field) {
$this->storeMeta($journal, $transaction, $field);
}
}
/**
* @param TransactionJournal $journal
* @param NullArrayObject $data
* @param string $field
*/
protected function storeMeta(TransactionJournal $journal, NullArrayObject $data, string $field): void
{
$set = [
'journal' => $journal,
'name' => $field,
'data' => (string)($data[$field] ?? ''),
];
if ($data[$field] instanceof Carbon) {
$data[$field]->setTimezone(config('app.timezone'));
Log::debug(sprintf('%s Date: %s (%s)', $field, $data[$field], $data[$field]->timezone->getName()));
$set['data'] = $data[$field]->format('Y-m-d H:i:s');
}
Log::debug(sprintf('Going to store meta-field "%s", with value "%s".', $set['name'], $set['data']));
/** @var TransactionJournalMetaFactory $factory */
$factory = app(TransactionJournalMetaFactory::class);
$factory->updateOrCreate($set);
}
/**
* @param bool $errorOnHash
*/
public function setErrorOnHash(bool $errorOnHash): void
{
$this->errorOnHash = $errorOnHash;
if (true === $errorOnHash) {
Log::info('Will trigger duplication alert for this journal.');
}
}
} }

View File

@@ -44,6 +44,7 @@ class MonthReportGenerator implements ReportGeneratorInterface
/** /**
* Generate the report. * Generate the report.
*
* @return string * @return string
* @throws FireflyException * @throws FireflyException
*/ */
@@ -68,6 +69,16 @@ class MonthReportGenerator implements ReportGeneratorInterface
return $result; return $result;
} }
/**
* Return the preferred period.
*
* @return string
*/
protected function preferredPeriod(): string
{
return 'day';
}
/** /**
* Set accounts. * Set accounts.
* *
@@ -159,14 +170,4 @@ class MonthReportGenerator implements ReportGeneratorInterface
{ {
return $this; return $this;
} }
/**
* Return the preferred period.
*
* @return string
*/
protected function preferredPeriod(): string
{
return 'day';
}
} }

View File

@@ -82,34 +82,6 @@ class MonthReportGenerator implements ReportGeneratorInterface
return $result; return $result;
} }
/**
* Set the involved accounts.
*
* @param Collection $accounts
*
* @return ReportGeneratorInterface
*/
public function setAccounts(Collection $accounts): ReportGeneratorInterface
{
$this->accounts = $accounts;
return $this;
}
/**
* Set the involved budgets.
*
* @param Collection $budgets
*
* @return ReportGeneratorInterface
*/
public function setBudgets(Collection $budgets): ReportGeneratorInterface
{
$this->budgets = $budgets;
return $this;
}
/** /**
* Unused category setter. * Unused category setter.
* *
@@ -200,4 +172,32 @@ class MonthReportGenerator implements ReportGeneratorInterface
return $journals; return $journals;
} }
/**
* Set the involved budgets.
*
* @param Collection $budgets
*
* @return ReportGeneratorInterface
*/
public function setBudgets(Collection $budgets): ReportGeneratorInterface
{
$this->budgets = $budgets;
return $this;
}
/**
* Set the involved accounts.
*
* @param Collection $accounts
*
* @return ReportGeneratorInterface
*/
public function setAccounts(Collection $accounts): ReportGeneratorInterface
{
$this->accounts = $accounts;
return $this;
}
} }

View File

@@ -82,20 +82,6 @@ class MonthReportGenerator implements ReportGeneratorInterface
} }
} }
/**
* Set the involved accounts.
*
* @param Collection $accounts
*
* @return ReportGeneratorInterface
*/
public function setAccounts(Collection $accounts): ReportGeneratorInterface
{
$this->accounts = $accounts;
return $this;
}
/** /**
* Empty budget setter. * Empty budget setter.
* *
@@ -108,20 +94,6 @@ class MonthReportGenerator implements ReportGeneratorInterface
return $this; return $this;
} }
/**
* Set the categories involved in this report.
*
* @param Collection $categories
*
* @return ReportGeneratorInterface
*/
public function setCategories(Collection $categories): ReportGeneratorInterface
{
$this->categories = $categories;
return $this;
}
/** /**
* Set the end date for this report. * Set the end date for this report.
* *
@@ -199,6 +171,34 @@ class MonthReportGenerator implements ReportGeneratorInterface
return $transactions; return $transactions;
} }
/**
* Set the categories involved in this report.
*
* @param Collection $categories
*
* @return ReportGeneratorInterface
*/
public function setCategories(Collection $categories): ReportGeneratorInterface
{
$this->categories = $categories;
return $this;
}
/**
* Set the involved accounts.
*
* @param Collection $accounts
*
* @return ReportGeneratorInterface
*/
public function setAccounts(Collection $accounts): ReportGeneratorInterface
{
$this->accounts = $accounts;
return $this;
}
/** /**
* Get the income for this report. * Get the income for this report.
* *

View File

@@ -81,62 +81,38 @@ class StandardMessageGenerator implements MessageGeneratorInterface
} }
/** /**
* @inheritDoc
*/
public function getVersion(): int
{
return $this->version;
}
/**
* @param Collection $objects
*/
public function setObjects(Collection $objects): void
{
$this->objects = $objects;
}
/**
* @param int $trigger
*/
public function setTrigger(int $trigger): void
{
$this->trigger = $trigger;
}
/**
* @param User $user
*/
public function setUser(User $user): void
{
$this->user = $user;
}
/**
* @inheritDoc
*/
public function setWebhooks(Collection $webhooks): void
{
$this->webhooks = $webhooks;
}
/**
* @param TransactionGroup $transactionGroup
*
* @return Collection * @return Collection
*/ */
private function collectAccounts(TransactionGroup $transactionGroup): Collection private function getWebhooks(): Collection
{ {
$accounts = new Collection(); return $this->user->webhooks()->where('active', true)->where('trigger', $this->trigger)->get(['webhooks.*']);
/** @var TransactionJournal $journal */
foreach ($transactionGroup->transactionJournals as $journal) {
/** @var Transaction $transaction */
foreach ($journal->transactions as $transaction) {
$accounts->push($transaction->account);
}
} }
return $accounts->unique(); /**
*
*/
private function run(): void
{
Log::debug('Now in StandardMessageGenerator::run');
/** @var Webhook $webhook */
foreach ($this->webhooks as $webhook) {
$this->runWebhook($webhook);
}
Log::debug('Done with StandardMessageGenerator::run');
}
/**
* @param Webhook $webhook
* @throws FireflyException
* @throws JsonException
*/
private function runWebhook(Webhook $webhook): void
{
Log::debug(sprintf('Now in runWebhook(#%d)', $webhook->id));
/** @var Model $object */
foreach ($this->objects as $object) {
$this->generateMessage($webhook, $object);
}
} }
/** /**
@@ -212,38 +188,30 @@ class StandardMessageGenerator implements MessageGeneratorInterface
} }
/** /**
* @inheritDoc
*/
public function getVersion(): int
{
return $this->version;
}
/**
* @param TransactionGroup $transactionGroup
*
* @return Collection * @return Collection
*/ */
private function getWebhooks(): Collection private function collectAccounts(TransactionGroup $transactionGroup): Collection
{ {
return $this->user->webhooks()->where('active', true)->where('trigger', $this->trigger)->get(['webhooks.*']); $accounts = new Collection();
/** @var TransactionJournal $journal */
foreach ($transactionGroup->transactionJournals as $journal) {
/** @var Transaction $transaction */
foreach ($journal->transactions as $transaction) {
$accounts->push($transaction->account);
}
} }
/** return $accounts->unique();
*
*/
private function run(): void
{
Log::debug('Now in StandardMessageGenerator::run');
/** @var Webhook $webhook */
foreach ($this->webhooks as $webhook) {
$this->runWebhook($webhook);
}
Log::debug('Done with StandardMessageGenerator::run');
}
/**
* @param Webhook $webhook
* @throws FireflyException
* @throws JsonException
*/
private function runWebhook(Webhook $webhook): void
{
Log::debug(sprintf('Now in runWebhook(#%d)', $webhook->id));
/** @var Model $object */
foreach ($this->objects as $object) {
$this->generateMessage($webhook, $object);
}
} }
/** /**
@@ -263,4 +231,36 @@ class StandardMessageGenerator implements MessageGeneratorInterface
$webhookMessage->save(); $webhookMessage->save();
Log::debug(sprintf('Stored new webhook message #%d', $webhookMessage->id)); Log::debug(sprintf('Stored new webhook message #%d', $webhookMessage->id));
} }
/**
* @param Collection $objects
*/
public function setObjects(Collection $objects): void
{
$this->objects = $objects;
}
/**
* @param int $trigger
*/
public function setTrigger(int $trigger): void
{
$this->trigger = $trigger;
}
/**
* @param User $user
*/
public function setUser(User $user): void
{
$this->user = $user;
}
/**
* @inheritDoc
*/
public function setWebhooks(Collection $webhooks): void
{
$this->webhooks = $webhooks;
}
} }

View File

@@ -53,25 +53,89 @@ class BudgetLimitHandler
} }
/** /**
* @param Deleted $event * @param BudgetLimit $budgetLimit
* @return void * @return void
*/ */
public function deleted(Deleted $event): void private function updateAvailableBudget(BudgetLimit $budgetLimit): void
{ {
Log::debug(sprintf('BudgetLimitHandler::deleted(#%s)', $event->budgetLimit->id)); Log::debug(sprintf('Now in updateAvailableBudget(#%d)', $budgetLimit->id));
$budgetLimit = $event->budgetLimit;
$budgetLimit->id = null; // based on the view range of the user (month week quarter etc) the budget limit could
$this->updateAvailableBudget($event->budgetLimit); // either overlap multiple available budget periods or be contained in a single one.
// all have to be created or updated.
try {
$viewRange = app('preferences')->get('viewRange', '1M')->data;
} catch (ContainerExceptionInterface | NotFoundExceptionInterface $e) {
$viewRange = '1M';
}
$start = app('navigation')->startOfPeriod($budgetLimit->start_date, $viewRange);
$end = app('navigation')->startOfPeriod($budgetLimit->end_date, $viewRange);
$end = app('navigation')->endOfPeriod($end, $viewRange);
$budget = Budget::find($budgetLimit->budget_id);
if (null === $budget) {
Log::warning('Budget is null, cannot continue.');
$budgetLimit->forceDelete();
return;
}
$user = $budget->user;
// sanity check. It happens when the budget has been deleted so the original user is unknown.
if (null === $user) {
Log::warning('User is null, cannot continue.');
$budgetLimit->forceDelete();
return;
} }
/** // limit period in total is:
* @param Updated $event $limitPeriod = Period::make($start, $end, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE());
* @return void
*/ // from the start until the end of the budget limit, need to loop!
public function updated(Updated $event): void $current = clone $start;
{ while ($current <= $end) {
Log::debug(sprintf('BudgetLimitHandler::updated(#%s)', $event->budgetLimit->id)); $currentEnd = app('navigation')->endOfPeriod($current, $viewRange);
$this->updateAvailableBudget($event->budgetLimit);
// create or find AB for this particular period, and set the amount accordingly.
/** @var AvailableBudget $availableBudget */
$availableBudget = $user->availableBudgets()->where('start_date', $current->format('Y-m-d'))->where(
'end_date',
$currentEnd->format('Y-m-d')
)->where('transaction_currency_id', $budgetLimit->transaction_currency_id)->first();
if (null !== $availableBudget) {
Log::debug('Found 1 AB, will update.');
$this->calculateAmount($availableBudget);
}
if (null === $availableBudget) {
// if not exists:
$currentPeriod = Period::make($current, $currentEnd, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE());
$daily = $this->getDailyAmount($budgetLimit);
$amount = bcmul($daily, (string)$currentPeriod->length(), 12);
// no need to calculate if period is equal.
if ($currentPeriod->equals($limitPeriod)) {
$amount = 0 === (int)$budgetLimit->id ? '0' : $budgetLimit->amount;
}
if (0 === bccomp($amount, '0')) {
Log::debug('Amount is zero, will not create AB.');
}
if (0 !== bccomp($amount, '0')) {
Log::debug(sprintf('Will create AB for period %s to %s', $current->format('Y-m-d'), $currentEnd->format('Y-m-d')));
$availableBudget = new AvailableBudget(
[
'user_id' => $budgetLimit->budget->user->id,
'transaction_currency_id' => $budgetLimit->transaction_currency_id,
'start_date' => $current,
'end_date' => $currentEnd,
'amount' => $amount,
]
);
$availableBudget->save();
}
}
// prep for next loop
$current = app('navigation')->addPeriod($current, $viewRange, 0);
}
} }
/** /**
@@ -163,89 +227,25 @@ class BudgetLimitHandler
} }
/** /**
* @param BudgetLimit $budgetLimit * @param Deleted $event
* @return void * @return void
*/ */
private function updateAvailableBudget(BudgetLimit $budgetLimit): void public function deleted(Deleted $event): void
{ {
Log::debug(sprintf('Now in updateAvailableBudget(#%d)', $budgetLimit->id)); Log::debug(sprintf('BudgetLimitHandler::deleted(#%s)', $event->budgetLimit->id));
$budgetLimit = $event->budgetLimit;
// based on the view range of the user (month week quarter etc) the budget limit could $budgetLimit->id = null;
// either overlap multiple available budget periods or be contained in a single one. $this->updateAvailableBudget($event->budgetLimit);
// all have to be created or updated.
try {
$viewRange = app('preferences')->get('viewRange', '1M')->data;
} catch (ContainerExceptionInterface|NotFoundExceptionInterface $e) {
$viewRange = '1M';
}
$start = app('navigation')->startOfPeriod($budgetLimit->start_date, $viewRange);
$end = app('navigation')->startOfPeriod($budgetLimit->end_date, $viewRange);
$end = app('navigation')->endOfPeriod($end, $viewRange);
$budget = Budget::find($budgetLimit->budget_id);
if (null === $budget) {
Log::warning('Budget is null, cannot continue.');
$budgetLimit->forceDelete();
return;
}
$user = $budget->user;
// sanity check. It happens when the budget has been deleted so the original user is unknown.
if (null === $user) {
Log::warning('User is null, cannot continue.');
$budgetLimit->forceDelete();
return;
} }
// limit period in total is: /**
$limitPeriod = Period::make($start, $end, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE()); * @param Updated $event
* @return void
// from the start until the end of the budget limit, need to loop! */
$current = clone $start; public function updated(Updated $event): void
while ($current <= $end) { {
$currentEnd = app('navigation')->endOfPeriod($current, $viewRange); Log::debug(sprintf('BudgetLimitHandler::updated(#%s)', $event->budgetLimit->id));
$this->updateAvailableBudget($event->budgetLimit);
// create or find AB for this particular period, and set the amount accordingly.
/** @var AvailableBudget $availableBudget */
$availableBudget = $user->availableBudgets()->where('start_date', $current->format('Y-m-d'))->where(
'end_date',
$currentEnd->format('Y-m-d')
)->where('transaction_currency_id', $budgetLimit->transaction_currency_id)->first();
if (null !== $availableBudget) {
Log::debug('Found 1 AB, will update.');
$this->calculateAmount($availableBudget);
}
if (null === $availableBudget) {
// if not exists:
$currentPeriod = Period::make($current, $currentEnd, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE());
$daily = $this->getDailyAmount($budgetLimit);
$amount = bcmul($daily, (string)$currentPeriod->length(), 12);
// no need to calculate if period is equal.
if ($currentPeriod->equals($limitPeriod)) {
$amount = 0 === (int)$budgetLimit->id ? '0' : $budgetLimit->amount;
}
if (0 === bccomp($amount, '0')) {
Log::debug('Amount is zero, will not create AB.');
}
if (0 !== bccomp($amount, '0')) {
Log::debug(sprintf('Will create AB for period %s to %s', $current->format('Y-m-d'), $currentEnd->format('Y-m-d')));
$availableBudget = new AvailableBudget(
[
'user_id' => $budgetLimit->budget->user->id,
'transaction_currency_id' => $budgetLimit->transaction_currency_id,
'start_date' => $current,
'end_date' => $currentEnd,
'amount' => $amount,
]
);
$availableBudget->save();
}
}
// prep for next loop
$current = app('navigation')->addPeriod($current, $viewRange, 0);
}
} }
} }

View File

@@ -305,6 +305,7 @@ class UserEventHandler
/** /**
* Send a new password to the user. * Send a new password to the user.
*
* @param RequestedNewPassword $event * @param RequestedNewPassword $event
*/ */
public function sendNewPassword(RequestedNewPassword $event): void public function sendNewPassword(RequestedNewPassword $event): void

View File

@@ -211,39 +211,6 @@ class AttachmentHelper implements AttachmentHelperInterface
return true; return true;
} }
/**
* Check if a model already has this file attached.
*
* @param UploadedFile $file
* @param Model $model
*
* @return bool
*/
protected function hasFile(UploadedFile $file, Model $model): bool
{
$md5 = md5_file($file->getRealPath());
$name = $file->getClientOriginalName();
$class = get_class($model);
$count = 0;
// ignore lines about polymorphic calls.
if ($model instanceof PiggyBank) {
$count = $model->account->user->attachments()->where('md5', $md5)->where('attachable_id', $model->id)->where('attachable_type', $class)->count();
}
if (!($model instanceof PiggyBank)) {
$count = $model->user->attachments()->where('md5', $md5)->where('attachable_id', $model->id)->where('attachable_type', $class)->count(
); // @phpstan-ignore-line
}
$result = false;
if ($count > 0) {
$msg = (string)trans('validation.file_already_attached', ['name' => $name]);
$this->errors->add('attachments', $msg);
Log::error($msg);
$result = true;
}
return $result;
}
/** /**
* Process the upload of a file. * Process the upload of a file.
* *
@@ -301,6 +268,39 @@ class AttachmentHelper implements AttachmentHelperInterface
return $attachment; return $attachment;
} }
/**
* Verify if the file was uploaded correctly.
*
* @param UploadedFile $file
* @param Model $model
*
* @return bool
*/
protected function validateUpload(UploadedFile $file, Model $model): bool
{
Log::debug('Now in validateUpload()');
$result = true;
if (!$this->validMime($file)) {
$result = false;
}
if (0 === $file->getSize()) {
Log::error('Cannot upload empty file.');
$result = false;
}
// can't seem to reach this point.
if (true === $result && !$this->validSize($file)) {
$result = false;
}
if (true === $result && $this->hasFile($file, $model)) {
$result = false;
}
return $result;
}
/** /**
* Verify if the mime of a file is valid. * Verify if the mime of a file is valid.
* *
@@ -353,33 +353,32 @@ class AttachmentHelper implements AttachmentHelperInterface
} }
/** /**
* Verify if the file was uploaded correctly. * Check if a model already has this file attached.
* *
* @param UploadedFile $file * @param UploadedFile $file
* @param Model $model * @param Model $model
* *
* @return bool * @return bool
*/ */
protected function validateUpload(UploadedFile $file, Model $model): bool protected function hasFile(UploadedFile $file, Model $model): bool
{ {
Log::debug('Now in validateUpload()'); $md5 = md5_file($file->getRealPath());
$name = $file->getClientOriginalName();
$class = get_class($model);
$count = 0;
// ignore lines about polymorphic calls.
if ($model instanceof PiggyBank) {
$count = $model->account->user->attachments()->where('md5', $md5)->where('attachable_id', $model->id)->where('attachable_type', $class)->count();
}
if (!($model instanceof PiggyBank)) {
$count = $model->user->attachments()->where('md5', $md5)->where('attachable_id', $model->id)->where('attachable_type', $class)->count(); // @phpstan-ignore-line
}
$result = false;
if ($count > 0) {
$msg = (string)trans('validation.file_already_attached', ['name' => $name]);
$this->errors->add('attachments', $msg);
Log::error($msg);
$result = true; $result = true;
if (!$this->validMime($file)) {
$result = false;
}
if (0 === $file->getSize()) {
Log::error('Cannot upload empty file.');
$result = false;
}
// can't seem to reach this point.
if (true === $result && !$this->validSize($file)) {
$result = false;
}
if (true === $result && $this->hasFile($file, $model)) {
$result = false;
} }
return $result; return $result;

View File

@@ -53,6 +53,25 @@ trait MetaCollection
return $this; return $this;
} }
/**
* Will include bill name + ID, if any.
*
* @return GroupCollectorInterface
*/
public function withBillInformation(): GroupCollectorInterface
{
if (false === $this->hasBillInformation) {
// join bill table
$this->query->leftJoin('bills', 'bills.id', '=', 'transaction_journals.bill_id');
// add fields
$this->fields[] = 'bills.id as bill_id';
$this->fields[] = 'bills.name as bill_name';
$this->hasBillInformation = true;
}
return $this;
}
/** /**
* Exclude a specific budget. * Exclude a specific budget.
* *
@@ -72,6 +91,27 @@ trait MetaCollection
return $this; return $this;
} }
/**
* Will include budget ID + name, if any.
*
* @return GroupCollectorInterface
*/
public function withBudgetInformation(): GroupCollectorInterface
{
if (false === $this->hasBudgetInformation) {
// join link table
$this->query->leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id');
// join cat table
$this->query->leftJoin('budgets', 'budget_transaction_journal.budget_id', '=', 'budgets.id');
// add fields
$this->fields[] = 'budgets.id as budget_id';
$this->fields[] = 'budgets.name as budget_name';
$this->hasBudgetInformation = true;
}
return $this;
}
/** /**
* @inheritDoc * @inheritDoc
*/ */
@@ -104,6 +144,27 @@ trait MetaCollection
return $this; return $this;
} }
/**
* Will include category ID + name, if any.
*
* @return GroupCollectorInterface
*/
public function withCategoryInformation(): GroupCollectorInterface
{
if (false === $this->hasCatInformation) {
// join link table
$this->query->leftJoin('category_transaction_journal', 'category_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id');
// join cat table
$this->query->leftJoin('categories', 'category_transaction_journal.category_id', '=', 'categories.id');
// add fields
$this->fields[] = 'categories.id as category_id';
$this->fields[] = 'categories.name as category_name';
$this->hasCatInformation = true;
}
return $this;
}
/** /**
* Exclude a specific category. * Exclude a specific category.
* *
@@ -135,6 +196,19 @@ trait MetaCollection
return $this; return $this;
} }
/**
* Join table to get tag information.
*/
protected function joinMetaDataTables(): void
{
if (false === $this->hasJoinedMetaTables) {
$this->hasJoinedMetaTables = true;
$this->query->leftJoin('journal_meta', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id');
$this->fields[] = 'journal_meta.name as meta_name';
$this->fields[] = 'journal_meta.data as meta_data';
}
}
/** /**
* @inheritDoc * @inheritDoc
*/ */
@@ -351,6 +425,37 @@ trait MetaCollection
return $this; return $this;
} }
/**
* @return GroupCollectorInterface
*/
public function withTagInformation(): GroupCollectorInterface
{
$this->fields[] = 'tags.id as tag_id';
$this->fields[] = 'tags.tag as tag_name';
$this->fields[] = 'tags.date as tag_date';
$this->fields[] = 'tags.description as tag_description';
$this->fields[] = 'tags.latitude as tag_latitude';
$this->fields[] = 'tags.longitude as tag_longitude';
$this->fields[] = 'tags.zoomLevel as tag_zoom_level';
$this->joinTagTables();
return $this;
}
/**
* Join table to get tag information.
*/
protected function joinTagTables(): void
{
if (false === $this->hasJoinedTagTables) {
// join some extra tables:
$this->hasJoinedTagTables = true;
$this->query->leftJoin('tag_transaction_journal', 'tag_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id');
$this->query->leftJoin('tags', 'tag_transaction_journal.tag_id', '=', 'tags.id');
}
}
/** /**
* @inheritDoc * @inheritDoc
*/ */
@@ -436,6 +541,29 @@ trait MetaCollection
return $this; return $this;
} }
/**
* @inheritDoc
*/
public function withNotes(): GroupCollectorInterface
{
if (false === $this->hasNotesInformation) {
// join bill table
$this->query->leftJoin(
'notes',
static function (JoinClause $join) {
$join->on('notes.noteable_id', '=', 'transaction_journals.id');
$join->where('notes.noteable_type', '=', 'FireflyIII\Models\TransactionJournal');
$join->whereNull('notes.deleted_at');
}
);
// add fields
$this->fields[] = 'notes.text as notes';
$this->hasNotesInformation = true;
}
return $this;
}
/** /**
* @param string $value * @param string $value
* *
@@ -778,25 +906,6 @@ trait MetaCollection
return $this; return $this;
} }
/**
* Will include bill name + ID, if any.
*
* @return GroupCollectorInterface
*/
public function withBillInformation(): GroupCollectorInterface
{
if (false === $this->hasBillInformation) {
// join bill table
$this->query->leftJoin('bills', 'bills.id', '=', 'transaction_journals.bill_id');
// add fields
$this->fields[] = 'bills.id as bill_id';
$this->fields[] = 'bills.name as bill_name';
$this->hasBillInformation = true;
}
return $this;
}
/** /**
* Limit results to a transactions without a budget.. * Limit results to a transactions without a budget..
* *
@@ -810,27 +919,6 @@ trait MetaCollection
return $this; return $this;
} }
/**
* Will include budget ID + name, if any.
*
* @return GroupCollectorInterface
*/
public function withBudgetInformation(): GroupCollectorInterface
{
if (false === $this->hasBudgetInformation) {
// join link table
$this->query->leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id');
// join cat table
$this->query->leftJoin('budgets', 'budget_transaction_journal.budget_id', '=', 'budgets.id');
// add fields
$this->fields[] = 'budgets.id as budget_id';
$this->fields[] = 'budgets.name as budget_name';
$this->hasBudgetInformation = true;
}
return $this;
}
/** /**
* Limit results to a transactions without a category. * Limit results to a transactions without a category.
* *
@@ -844,27 +932,6 @@ trait MetaCollection
return $this; return $this;
} }
/**
* Will include category ID + name, if any.
*
* @return GroupCollectorInterface
*/
public function withCategoryInformation(): GroupCollectorInterface
{
if (false === $this->hasCatInformation) {
// join link table
$this->query->leftJoin('category_transaction_journal', 'category_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id');
// join cat table
$this->query->leftJoin('categories', 'category_transaction_journal.category_id', '=', 'categories.id');
// add fields
$this->fields[] = 'categories.id as category_id';
$this->fields[] = 'categories.name as category_name';
$this->hasCatInformation = true;
}
return $this;
}
/** /**
* @inheritDoc * @inheritDoc
*/ */
@@ -889,47 +956,6 @@ trait MetaCollection
return $this; return $this;
} }
/**
* @inheritDoc
*/
public function withNotes(): GroupCollectorInterface
{
if (false === $this->hasNotesInformation) {
// join bill table
$this->query->leftJoin(
'notes',
static function (JoinClause $join) {
$join->on('notes.noteable_id', '=', 'transaction_journals.id');
$join->where('notes.noteable_type', '=', 'FireflyIII\Models\TransactionJournal');
$join->whereNull('notes.deleted_at');
}
);
// add fields
$this->fields[] = 'notes.text as notes';
$this->hasNotesInformation = true;
}
return $this;
}
/**
* @return GroupCollectorInterface
*/
public function withTagInformation(): GroupCollectorInterface
{
$this->fields[] = 'tags.id as tag_id';
$this->fields[] = 'tags.tag as tag_name';
$this->fields[] = 'tags.date as tag_date';
$this->fields[] = 'tags.description as tag_description';
$this->fields[] = 'tags.latitude as tag_latitude';
$this->fields[] = 'tags.longitude as tag_longitude';
$this->fields[] = 'tags.zoomLevel as tag_zoom_level';
$this->joinTagTables();
return $this;
}
/** /**
* Limit results to a transactions without a bill. * Limit results to a transactions without a bill.
* *
@@ -1036,30 +1062,4 @@ trait MetaCollection
return $this; return $this;
} }
/**
* Join table to get tag information.
*/
protected function joinMetaDataTables(): void
{
if (false === $this->hasJoinedMetaTables) {
$this->hasJoinedMetaTables = true;
$this->query->leftJoin('journal_meta', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id');
$this->fields[] = 'journal_meta.name as meta_name';
$this->fields[] = 'journal_meta.data as meta_data';
}
}
/**
* Join table to get tag information.
*/
protected function joinTagTables(): void
{
if (false === $this->hasJoinedTagTables) {
// join some extra tables:
$this->hasJoinedTagTables = true;
$this->query->leftJoin('tag_transaction_journal', 'tag_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id');
$this->query->leftJoin('tags', 'tag_transaction_journal.tag_id', '=', 'tags.id');
}
}
} }

View File

@@ -102,6 +102,18 @@ trait TimeCollection
return $this; return $this;
} }
/**
* @inheritDoc
*/
public function withMetaDate(string $field): GroupCollectorInterface
{
$this->joinMetaDataTables();
$this->query->where('journal_meta.name', '=', $field);
$this->query->whereNotNull('journal_meta.data');
return $this;
}
/** /**
* @param Carbon $start * @param Carbon $start
* @param Carbon $end * @param Carbon $end
@@ -786,18 +798,6 @@ trait TimeCollection
return $this; return $this;
} }
/**
* @inheritDoc
*/
public function withMetaDate(string $field): GroupCollectorInterface
{
$this->joinMetaDataTables();
$this->query->where('journal_meta.name', '=', $field);
$this->query->whereNotNull('journal_meta.data');
return $this;
}
/** /**
* @param string $year * @param string $year
* @return GroupCollectorInterface * @return GroupCollectorInterface

View File

@@ -504,312 +504,6 @@ class GroupCollector implements GroupCollectorInterface
return $collection; return $collection;
} }
/**
* Same as getGroups but everything is in a paginator.
*
* @return LengthAwarePaginator
*/
public function getPaginatedGroups(): LengthAwarePaginator
{
$set = $this->getGroups();
if (0 === $this->limit) {
$this->setLimit(50);
}
return new LengthAwarePaginator($set, $this->total, $this->limit, $this->page);
}
/**
* @inheritDoc
*/
public function isNotReconciled(): GroupCollectorInterface
{
$this->query->where('source.reconciled', 0)->where('destination.reconciled', 0);
return $this;
}
/**
* @inheritDoc
*/
public function isReconciled(): GroupCollectorInterface
{
$this->query->where('source.reconciled', 1)->where('destination.reconciled', 1);
return $this;
}
/**
* Limit results to a specific currency, either foreign or normal one.
*
* @param TransactionCurrency $currency
*
* @return GroupCollectorInterface
*/
public function setCurrency(TransactionCurrency $currency): GroupCollectorInterface
{
$this->query->where(
static function (EloquentBuilder $q) use ($currency) {
$q->where('source.transaction_currency_id', $currency->id);
$q->orWhere('source.foreign_currency_id', $currency->id);
}
);
return $this;
}
/**
* @inheritDoc
*/
public function setForeignCurrency(TransactionCurrency $currency): GroupCollectorInterface
{
$this->query->where('source.foreign_currency_id', $currency->id);
return $this;
}
/**
* Limit the result to a set of specific transaction groups.
*
* @param array $groupIds
*
* @return GroupCollectorInterface
*/
public function setIds(array $groupIds): GroupCollectorInterface
{
$this->query->whereIn('transaction_groups.id', $groupIds);
return $this;
}
/**
* Limit the result to a set of specific journals.
*
* @param array $journalIds
*
* @return GroupCollectorInterface
*/
public function setJournalIds(array $journalIds): GroupCollectorInterface
{
if (0 !== count($journalIds)) {
// make all integers.
$integerIDs = array_map('intval', $journalIds);
$this->query->whereIn('transaction_journals.id', $integerIDs);
}
return $this;
}
/**
* Limit the number of returned entries.
*
* @param int $limit
*
* @return GroupCollectorInterface
*/
public function setLimit(int $limit): GroupCollectorInterface
{
$this->limit = $limit;
app('log')->debug(sprintf('GroupCollector: The limit is now %d', $limit));
return $this;
}
/**
* Set the page to get.
*
* @param int $page
*
* @return GroupCollectorInterface
*/
public function setPage(int $page): GroupCollectorInterface
{
$page = 0 === $page ? 1 : $page;
$this->page = $page;
app('log')->debug(sprintf('GroupCollector: page is now %d', $page));
return $this;
}
/**
* Search for words in descriptions.
*
* @param array $array
*
* @return GroupCollectorInterface
*/
public function setSearchWords(array $array): GroupCollectorInterface
{
if (0 === count($array)) {
return $this;
}
$this->query->where(
static function (EloquentBuilder $q) use ($array) {
$q->where(
static function (EloquentBuilder $q1) use ($array) {
foreach ($array as $word) {
$keyword = sprintf('%%%s%%', $word);
$q1->where('transaction_journals.description', 'LIKE', $keyword);
}
}
);
$q->orWhere(
static function (EloquentBuilder $q2) use ($array) {
foreach ($array as $word) {
$keyword = sprintf('%%%s%%', $word);
$q2->where('transaction_groups.title', 'LIKE', $keyword);
}
}
);
}
);
return $this;
}
/**
* Limit the search to one specific transaction group.
*
* @param TransactionGroup $transactionGroup
*
* @return GroupCollectorInterface
*/
public function setTransactionGroup(TransactionGroup $transactionGroup): GroupCollectorInterface
{
$this->query->where('transaction_groups.id', $transactionGroup->id);
return $this;
}
/**
* Limit the included transaction types.
*
* @param array $types
*
* @return GroupCollectorInterface
*/
public function setTypes(array $types): GroupCollectorInterface
{
$this->query->whereIn('transaction_types.type', $types);
return $this;
}
/**
* Set the user object and start the query.
*
* @param User $user
*
* @return GroupCollectorInterface
*/
public function setUser(User $user): GroupCollectorInterface
{
if (null === $this->user) {
$this->user = $user;
$this->startQuery();
}
return $this;
}
/**
* Automatically include all stuff required to make API calls work.
*
* @return GroupCollectorInterface
*/
public function withAPIInformation(): GroupCollectorInterface
{
// include source + destination account name and type.
$this->withAccountInformation()
// include category ID + name (if any)
->withCategoryInformation()
// include budget ID + name (if any)
->withBudgetInformation()
// include bill ID + name (if any)
->withBillInformation();
return $this;
}
/**
* Convert a selected set of fields to arrays.
*
* @param array $array
*
* @return array
*/
private function convertToInteger(array $array): array
{
foreach ($this->integerFields as $field) {
$array[$field] = array_key_exists($field, $array) ? (int)$array[$field] : null;
}
return $array;
}
/**
* @param array $array
* @return array
*/
private function convertToStrings(array $array): array
{
foreach ($this->stringFields as $field) {
$array[$field] = array_key_exists($field, $array) && null !== $array[$field] ? (string)$array[$field] : null;
}
return $array;
}
/**
* @param array $existingJournal
* @param TransactionJournal $newJournal
*
* @return array
*/
private function mergeAttachments(array $existingJournal, TransactionJournal $newJournal): array
{
$newArray = $newJournal->toArray();
if (array_key_exists('attachment_id', $newArray)) {
$attachmentId = (int)$newJournal['attachment_id'];
$existingJournal['attachments'][$attachmentId] = [
'id' => $attachmentId,
];
}
return $existingJournal;
}
/**
* @param array $existingJournal
* @param TransactionJournal $newJournal
*
* @return array
*/
private function mergeTags(array $existingJournal, TransactionJournal $newJournal): array
{
$newArray = $newJournal->toArray();
if (array_key_exists('tag_id', $newArray)) { // assume the other fields are present as well.
$tagId = (int)$newJournal['tag_id'];
$tagDate = null;
try {
$tagDate = Carbon::parse($newArray['tag_date']);
} catch (InvalidFormatException $e) {
Log::debug(sprintf('Could not parse date: %s', $e->getMessage()));
}
$existingJournal['tags'][$tagId] = [
'id' => (int)$newArray['tag_id'],
'name' => $newArray['tag_name'],
'date' => $tagDate,
'description' => $newArray['tag_description'],
];
}
return $existingJournal;
}
/** /**
* @param Collection $collection * @param Collection $collection
* *
@@ -951,6 +645,85 @@ class GroupCollector implements GroupCollectorInterface
return $result; return $result;
} }
/**
* Convert a selected set of fields to arrays.
*
* @param array $array
*
* @return array
*/
private function convertToInteger(array $array): array
{
foreach ($this->integerFields as $field) {
$array[$field] = array_key_exists($field, $array) ? (int)$array[$field] : null;
}
return $array;
}
/**
* @param array $array
* @return array
*/
private function convertToStrings(array $array): array
{
foreach ($this->stringFields as $field) {
$array[$field] = array_key_exists($field, $array) && null !== $array[$field] ? (string)$array[$field] : null;
}
return $array;
}
/**
* @param array $existingJournal
* @param TransactionJournal $newJournal
*
* @return array
*/
private function mergeTags(array $existingJournal, TransactionJournal $newJournal): array
{
$newArray = $newJournal->toArray();
if (array_key_exists('tag_id', $newArray)) { // assume the other fields are present as well.
$tagId = (int)$newJournal['tag_id'];
$tagDate = null;
try {
$tagDate = Carbon::parse($newArray['tag_date']);
} catch (InvalidFormatException $e) {
Log::debug(sprintf('Could not parse date: %s', $e->getMessage()));
}
$existingJournal['tags'][$tagId] = [
'id' => (int)$newArray['tag_id'],
'name' => $newArray['tag_name'],
'date' => $tagDate,
'description' => $newArray['tag_description'],
];
}
return $existingJournal;
}
/**
* @param array $existingJournal
* @param TransactionJournal $newJournal
*
* @return array
*/
private function mergeAttachments(array $existingJournal, TransactionJournal $newJournal): array
{
$newArray = $newJournal->toArray();
if (array_key_exists('attachment_id', $newArray)) {
$attachmentId = (int)$newJournal['attachment_id'];
$existingJournal['attachments'][$attachmentId] = [
'id' => $attachmentId,
];
}
return $existingJournal;
}
/** /**
* @param array $groups * @param array $groups
* *
@@ -1028,6 +801,214 @@ class GroupCollector implements GroupCollectorInterface
return $currentCollection; return $currentCollection;
} }
/**
* Same as getGroups but everything is in a paginator.
*
* @return LengthAwarePaginator
*/
public function getPaginatedGroups(): LengthAwarePaginator
{
$set = $this->getGroups();
if (0 === $this->limit) {
$this->setLimit(50);
}
return new LengthAwarePaginator($set, $this->total, $this->limit, $this->page);
}
/**
* Limit the number of returned entries.
*
* @param int $limit
*
* @return GroupCollectorInterface
*/
public function setLimit(int $limit): GroupCollectorInterface
{
$this->limit = $limit;
app('log')->debug(sprintf('GroupCollector: The limit is now %d', $limit));
return $this;
}
/**
* @inheritDoc
*/
public function isNotReconciled(): GroupCollectorInterface
{
$this->query->where('source.reconciled', 0)->where('destination.reconciled', 0);
return $this;
}
/**
* @inheritDoc
*/
public function isReconciled(): GroupCollectorInterface
{
$this->query->where('source.reconciled', 1)->where('destination.reconciled', 1);
return $this;
}
/**
* Limit results to a specific currency, either foreign or normal one.
*
* @param TransactionCurrency $currency
*
* @return GroupCollectorInterface
*/
public function setCurrency(TransactionCurrency $currency): GroupCollectorInterface
{
$this->query->where(
static function (EloquentBuilder $q) use ($currency) {
$q->where('source.transaction_currency_id', $currency->id);
$q->orWhere('source.foreign_currency_id', $currency->id);
}
);
return $this;
}
/**
* @inheritDoc
*/
public function setForeignCurrency(TransactionCurrency $currency): GroupCollectorInterface
{
$this->query->where('source.foreign_currency_id', $currency->id);
return $this;
}
/**
* Limit the result to a set of specific transaction groups.
*
* @param array $groupIds
*
* @return GroupCollectorInterface
*/
public function setIds(array $groupIds): GroupCollectorInterface
{
$this->query->whereIn('transaction_groups.id', $groupIds);
return $this;
}
/**
* Limit the result to a set of specific journals.
*
* @param array $journalIds
*
* @return GroupCollectorInterface
*/
public function setJournalIds(array $journalIds): GroupCollectorInterface
{
if (0 !== count($journalIds)) {
// make all integers.
$integerIDs = array_map('intval', $journalIds);
$this->query->whereIn('transaction_journals.id', $integerIDs);
}
return $this;
}
/**
* Set the page to get.
*
* @param int $page
*
* @return GroupCollectorInterface
*/
public function setPage(int $page): GroupCollectorInterface
{
$page = 0 === $page ? 1 : $page;
$this->page = $page;
app('log')->debug(sprintf('GroupCollector: page is now %d', $page));
return $this;
}
/**
* Search for words in descriptions.
*
* @param array $array
*
* @return GroupCollectorInterface
*/
public function setSearchWords(array $array): GroupCollectorInterface
{
if (0 === count($array)) {
return $this;
}
$this->query->where(
static function (EloquentBuilder $q) use ($array) {
$q->where(
static function (EloquentBuilder $q1) use ($array) {
foreach ($array as $word) {
$keyword = sprintf('%%%s%%', $word);
$q1->where('transaction_journals.description', 'LIKE', $keyword);
}
}
);
$q->orWhere(
static function (EloquentBuilder $q2) use ($array) {
foreach ($array as $word) {
$keyword = sprintf('%%%s%%', $word);
$q2->where('transaction_groups.title', 'LIKE', $keyword);
}
}
);
}
);
return $this;
}
/**
* Limit the search to one specific transaction group.
*
* @param TransactionGroup $transactionGroup
*
* @return GroupCollectorInterface
*/
public function setTransactionGroup(TransactionGroup $transactionGroup): GroupCollectorInterface
{
$this->query->where('transaction_groups.id', $transactionGroup->id);
return $this;
}
/**
* Limit the included transaction types.
*
* @param array $types
*
* @return GroupCollectorInterface
*/
public function setTypes(array $types): GroupCollectorInterface
{
$this->query->whereIn('transaction_types.type', $types);
return $this;
}
/**
* Set the user object and start the query.
*
* @param User $user
*
* @return GroupCollectorInterface
*/
public function setUser(User $user): GroupCollectorInterface
{
if (null === $this->user) {
$this->user = $user;
$this->startQuery();
}
return $this;
}
/** /**
* Build the query. * Build the query.
*/ */
@@ -1070,4 +1051,23 @@ class GroupCollector implements GroupCollectorInterface
->orderBy('transaction_journals.description', 'DESC') ->orderBy('transaction_journals.description', 'DESC')
->orderBy('source.amount', 'DESC'); ->orderBy('source.amount', 'DESC');
} }
/**
* Automatically include all stuff required to make API calls work.
*
* @return GroupCollectorInterface
*/
public function withAPIInformation(): GroupCollectorInterface
{
// include source + destination account name and type.
$this->withAccountInformation()
// include category ID + name (if any)
->withCategoryInformation()
// include budget ID + name (if any)
->withBudgetInformation()
// include bill ID + name (if any)
->withBillInformation();
return $this;
}
} }

View File

@@ -128,6 +128,37 @@ class LoginController extends Controller
$this->sendFailedLoginResponse($request); $this->sendFailedLoginResponse($request);
} }
/**
* Get the login username to be used by the controller.
*
* @return string
*/
public function username()
{
return $this->username;
}
/**
* Get the failed login response instance.
*
* @param Request $request
*
* @return void
*
* @throws ValidationException
*/
protected function sendFailedLoginResponse(Request $request)
{
$exception = ValidationException::withMessages(
[
$this->username() => [trans('auth.failed')],
]
);
$exception->redirectTo = route('login');
throw $exception;
}
/** /**
* Log the user out of the application. * Log the user out of the application.
* *
@@ -213,35 +244,4 @@ class LoginController extends Controller
return view('auth.login', compact('allowRegistration', 'email', 'remember', 'allowReset', 'title', 'usernameField')); return view('auth.login', compact('allowRegistration', 'email', 'remember', 'allowReset', 'title', 'usernameField'));
} }
/**
* Get the login username to be used by the controller.
*
* @return string
*/
public function username()
{
return $this->username;
}
/**
* Get the failed login response instance.
*
* @param Request $request
*
* @return void
*
* @throws ValidationException
*/
protected function sendFailedLoginResponse(Request $request)
{
$exception = ValidationException::withMessages(
[
$this->username() => [trans('auth.failed')],
]
);
$exception->redirectTo = route('login');
throw $exception;
}
} }

View File

@@ -114,6 +114,30 @@ class RegisterController extends Controller
return redirect($this->redirectPath()); return redirect($this->redirectPath());
} }
/**
* @return bool
* @throws FireflyException
*/
protected function allowedToRegister(): bool
{
// is allowed to register?
$allowRegistration = true;
try {
$singleUserMode = app('fireflyconfig')->get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
} catch (ContainerExceptionInterface | NotFoundExceptionInterface $e) {
$singleUserMode = true;
}
$userCount = User::count();
$guard = config('auth.defaults.guard');
if (true === $singleUserMode && $userCount > 0 && 'web' === $guard) {
$allowRegistration = false;
}
if ('web' !== $guard) {
$allowRegistration = false;
}
return $allowRegistration;
}
/** /**
* Show the application registration form if the invitation code is valid. * Show the application registration form if the invitation code is valid.
* *
@@ -174,28 +198,4 @@ class RegisterController extends Controller
return view('auth.register', compact('isDemoSite', 'email', 'pageTitle')); return view('auth.register', compact('isDemoSite', 'email', 'pageTitle'));
} }
/**
* @return bool
* @throws FireflyException
*/
protected function allowedToRegister(): bool
{
// is allowed to register?
$allowRegistration = true;
try {
$singleUserMode = app('fireflyconfig')->get('single_user_mode', config('firefly.configuration.single_user_mode'))->data;
} catch (ContainerExceptionInterface|NotFoundExceptionInterface $e) {
$singleUserMode = true;
}
$userCount = User::count();
$guard = config('auth.defaults.guard');
if (true === $singleUserMode && $userCount > 0 && 'web' === $guard) {
$allowRegistration = false;
}
if ('web' !== $guard) {
$allowRegistration = false;
}
return $allowRegistration;
}
} }

View File

@@ -99,20 +99,26 @@ class TwoFactorController extends Controller
} }
/** /**
* Each MFA history has a timestamp and a code, saving the MFA entries for 5 minutes. So if the
* submitted MFA code has been submitted in the last 5 minutes, it won't work despite being valid.
*
* @param string $mfaCode * @param string $mfaCode
* @param array $mfaHistory
*
* @return bool
*/ */
private function addToMFAHistory(string $mfaCode): void private function inMFAHistory(string $mfaCode, array $mfaHistory): bool
{ {
/** @var array $mfaHistory */ $now = time();
$mfaHistory = Preferences::get('mfa_history', [])->data; foreach ($mfaHistory as $entry) {
$entry = [ $time = $entry['time'];
'time' => time(), $code = $entry['code'];
'code' => $mfaCode, if ($code === $mfaCode && $now - $time <= 300) {
]; return true;
$mfaHistory[] = $entry; }
}
Preferences::set('mfa_history', $mfaHistory); return false;
$this->filterMFAHistory();
} }
/** /**
@@ -138,26 +144,20 @@ class TwoFactorController extends Controller
} }
/** /**
* Each MFA history has a timestamp and a code, saving the MFA entries for 5 minutes. So if the
* submitted MFA code has been submitted in the last 5 minutes, it won't work despite being valid.
*
* @param string $mfaCode * @param string $mfaCode
* @param array $mfaHistory
*
* @return bool
*/ */
private function inMFAHistory(string $mfaCode, array $mfaHistory): bool private function addToMFAHistory(string $mfaCode): void
{ {
$now = time(); /** @var array $mfaHistory */
foreach ($mfaHistory as $entry) { $mfaHistory = Preferences::get('mfa_history', [])->data;
$time = $entry['time']; $entry = [
$code = $entry['code']; 'time' => time(),
if ($code === $mfaCode && $now - $time <= 300) { 'code' => $mfaCode,
return true; ];
} $mfaHistory[] = $entry;
}
return false; Preferences::set('mfa_history', $mfaHistory);
$this->filterMFAHistory();
} }
/** /**

View File

@@ -135,26 +135,49 @@ class IndexController extends Controller
} }
/** /**
* Set the order of a bill. * @param array $bills
* *
* @param Request $request * @return array
* @param Bill $bill * @throws FireflyException
* * @throws ContainerExceptionInterface
* @return JsonResponse * @throws NotFoundExceptionInterface
*/ */
public function setOrder(Request $request, Bill $bill): JsonResponse private function getSums(array $bills): array
{ {
$objectGroupTitle = (string)$request->get('objectGroupTitle'); $sums = [];
$newOrder = (int)$request->get('order'); $range = app('navigation')->getViewRange(false);
$this->repository->setOrder($bill, $newOrder);
if ('' !== $objectGroupTitle) { /** @var array $group */
$this->repository->setObjectGroup($bill, $objectGroupTitle); foreach ($bills as $groupOrder => $group) {
} /** @var array $bill */
if ('' === $objectGroupTitle) { foreach ($group['bills'] as $bill) {
$this->repository->removeObjectGroup($bill); if (false === $bill['active']) {
continue;
} }
return response()->json(['data' => 'OK']); $currencyId = $bill['currency_id'];
$sums[$groupOrder][$currencyId] = $sums[$groupOrder][$currencyId] ?? [
'currency_id' => $currencyId,
'currency_code' => $bill['currency_code'],
'currency_name' => $bill['currency_name'],
'currency_symbol' => $bill['currency_symbol'],
'currency_decimal_places' => $bill['currency_decimal_places'],
'avg' => '0',
'period' => $range,
'per_period' => '0',
];
// only fill in avg when bill is active.
if (count($bill['pay_dates']) > 0) {
$avg = bcdiv(bcadd((string)$bill['amount_min'], (string)$bill['amount_max']), '2');
$avg = bcmul($avg, (string)count($bill['pay_dates']));
$sums[$groupOrder][$currencyId]['avg'] = bcadd($sums[$groupOrder][$currencyId]['avg'], $avg);
}
// fill in per period regardless:
$sums[$groupOrder][$currencyId]['per_period'] = bcadd($sums[$groupOrder][$currencyId]['per_period'], $this->amountPerPeriod($bill, $range));
}
}
return $sums;
} }
/** /**
@@ -204,52 +227,6 @@ class IndexController extends Controller
return $perPeriod; return $perPeriod;
} }
/**
* @param array $bills
*
* @return array
* @throws FireflyException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getSums(array $bills): array
{
$sums = [];
$range = app('navigation')->getViewRange(false);
/** @var array $group */
foreach ($bills as $groupOrder => $group) {
/** @var array $bill */
foreach ($group['bills'] as $bill) {
if (false === $bill['active']) {
continue;
}
$currencyId = $bill['currency_id'];
$sums[$groupOrder][$currencyId] = $sums[$groupOrder][$currencyId] ?? [
'currency_id' => $currencyId,
'currency_code' => $bill['currency_code'],
'currency_name' => $bill['currency_name'],
'currency_symbol' => $bill['currency_symbol'],
'currency_decimal_places' => $bill['currency_decimal_places'],
'avg' => '0',
'period' => $range,
'per_period' => '0',
];
// only fill in avg when bill is active.
if (count($bill['pay_dates']) > 0) {
$avg = bcdiv(bcadd((string)$bill['amount_min'], (string)$bill['amount_max']), '2');
$avg = bcmul($avg, (string)count($bill['pay_dates']));
$sums[$groupOrder][$currencyId]['avg'] = bcadd($sums[$groupOrder][$currencyId]['avg'], $avg);
}
// fill in per period regardless:
$sums[$groupOrder][$currencyId]['per_period'] = bcadd($sums[$groupOrder][$currencyId]['per_period'], $this->amountPerPeriod($bill, $range));
}
}
return $sums;
}
/** /**
* @param array $sums * @param array $sums
* *
@@ -287,4 +264,27 @@ class IndexController extends Controller
return $totals; return $totals;
} }
/**
* Set the order of a bill.
*
* @param Request $request
* @param Bill $bill
*
* @return JsonResponse
*/
public function setOrder(Request $request, Bill $bill): JsonResponse
{
$objectGroupTitle = (string)$request->get('objectGroupTitle');
$newOrder = (int)$request->get('order');
$this->repository->setOrder($bill, $newOrder);
if ('' !== $objectGroupTitle) {
$this->repository->setObjectGroup($bill, $objectGroupTitle);
}
if ('' === $objectGroupTitle) {
$this->repository->removeObjectGroup($bill);
}
return response()->json(['data' => 'OK']);
}
} }

View File

@@ -122,6 +122,7 @@ class BudgetLimitController extends Controller
/** /**
* TODO why redirect AND json response? * TODO why redirect AND json response?
*
* @param Request $request * @param Request $request
* *
* @return RedirectResponse|JsonResponse * @return RedirectResponse|JsonResponse

View File

@@ -173,29 +173,6 @@ class IndexController extends Controller
); );
} }
/**
* @param Request $request
* @param BudgetRepositoryInterface $repository
*
* @return JsonResponse
*/
public function reorder(Request $request, BudgetRepositoryInterface $repository): JsonResponse
{
$budgetIds = $request->get('budgetIds');
foreach ($budgetIds as $index => $budgetId) {
$budgetId = (int)$budgetId;
$budget = $repository->find($budgetId);
if (null !== $budget) {
Log::debug(sprintf('Set budget #%d ("%s") to position %d', $budget->id, $budget->name, $index + 1));
$repository->setBudgetOrder($budget, $index + 1);
}
}
app('preferences')->mark();
return response()->json(['OK']);
}
/** /**
* @param Carbon $start * @param Carbon $start
* @param Carbon $end * @param Carbon $end
@@ -351,4 +328,27 @@ class IndexController extends Controller
return $sums; return $sums;
} }
/**
* @param Request $request
* @param BudgetRepositoryInterface $repository
*
* @return JsonResponse
*/
public function reorder(Request $request, BudgetRepositoryInterface $repository): JsonResponse
{
$budgetIds = $request->get('budgetIds');
foreach ($budgetIds as $index => $budgetId) {
$budgetId = (int)$budgetId;
$budget = $repository->find($budgetId);
if (null !== $budget) {
Log::debug(sprintf('Set budget #%d ("%s") to position %d', $budget->id, $budget->name, $index + 1));
$repository->setBudgetOrder($budget, $index + 1);
}
}
app('preferences')->mark();
return response()->json(['OK']);
}
} }

View File

@@ -173,6 +173,22 @@ class AccountController extends Controller
return response()->json($data); return response()->json($data);
} }
/**
* Expenses per budget for all time, as shown on account overview.
*
* @param AccountRepositoryInterface $repository
* @param Account $account
*
* @return JsonResponse
*/
public function expenseBudgetAll(AccountRepositoryInterface $repository, Account $account): JsonResponse
{
$start = $repository->oldestJournalDate($account) ?? today(config('app.timezone'))->startOfMonth();
$end = today(config('app.timezone'));
return $this->expenseBudget($account, $start, $end);
}
/** /**
* Expenses per budget, as shown on account overview. * Expenses per budget, as shown on account overview.
* *
@@ -232,19 +248,19 @@ class AccountController extends Controller
} }
/** /**
* Expenses per budget for all time, as shown on account overview. * Expenses grouped by category for account.
* *
* @param AccountRepositoryInterface $repository * @param AccountRepositoryInterface $repository
* @param Account $account * @param Account $account
* *
* @return JsonResponse * @return JsonResponse
*/ */
public function expenseBudgetAll(AccountRepositoryInterface $repository, Account $account): JsonResponse public function expenseCategoryAll(AccountRepositoryInterface $repository, Account $account): JsonResponse
{ {
$start = $repository->oldestJournalDate($account) ?? today(config('app.timezone'))->startOfMonth(); $start = $repository->oldestJournalDate($account) ?? today(config('app.timezone'))->startOfMonth();
$end = today(config('app.timezone')); $end = today(config('app.timezone'));
return $this->expenseBudget($account, $start, $end); return $this->expenseCategory($account, $start, $end);
} }
/** /**
@@ -303,22 +319,6 @@ class AccountController extends Controller
return response()->json($data); return response()->json($data);
} }
/**
* Expenses grouped by category for account.
*
* @param AccountRepositoryInterface $repository
* @param Account $account
*
* @return JsonResponse
*/
public function expenseCategoryAll(AccountRepositoryInterface $repository, Account $account): JsonResponse
{
$start = $repository->oldestJournalDate($account) ?? today(config('app.timezone'))->startOfMonth();
$end = today(config('app.timezone'));
return $this->expenseCategory($account, $start, $end);
}
/** /**
* Shows the balances for all the user's frontpage accounts. * Shows the balances for all the user's frontpage accounts.
* *
@@ -347,6 +347,22 @@ class AccountController extends Controller
return response()->json($this->accountBalanceChart($accounts, $start, $end)); return response()->json($this->accountBalanceChart($accounts, $start, $end));
} }
/**
* Shows the income grouped by category for an account, in all time.
*
* @param AccountRepositoryInterface $repository
* @param Account $account
*
* @return JsonResponse
*/
public function incomeCategoryAll(AccountRepositoryInterface $repository, Account $account): JsonResponse
{
$start = $repository->oldestJournalDate($account) ?? today(config('app.timezone'))->startOfMonth();
$end = today(config('app.timezone'));
return $this->incomeCategory($account, $start, $end);
}
/** /**
* Shows all income per account for each category. * Shows all income per account for each category.
* *
@@ -403,22 +419,6 @@ class AccountController extends Controller
return response()->json($data); return response()->json($data);
} }
/**
* Shows the income grouped by category for an account, in all time.
*
* @param AccountRepositoryInterface $repository
* @param Account $account
*
* @return JsonResponse
*/
public function incomeCategoryAll(AccountRepositoryInterface $repository, Account $account): JsonResponse
{
$start = $repository->oldestJournalDate($account) ?? today(config('app.timezone'))->startOfMonth();
$end = today(config('app.timezone'));
return $this->incomeCategory($account, $start, $end);
}
/** /**
* Shows overview of account during a single period. * Shows overview of account during a single period.
* *
@@ -460,6 +460,54 @@ class AccountController extends Controller
return response()->json($data); return response()->json($data);
} }
/**
* @param Carbon $start
* @param Carbon $end
* @param Account $account
* @param TransactionCurrency $currency
*
* @return array
* @throws FireflyException
* @throws JsonException
*/
private function periodByCurrency(Carbon $start, Carbon $end, Account $account, TransactionCurrency $currency): array
{
$locale = app('steam')->getLocale();
$step = $this->calculateStep($start, $end);
$result = [
'label' => sprintf('%s (%s)', $account->name, $currency->symbol),
'currency_symbol' => $currency->symbol,
'currency_code' => $currency->code,
];
$entries = [];
$current = clone $start;
if ('1D' === $step) {
// per day the entire period, balance for every day.
$format = (string)trans('config.month_and_day_js', [], $locale);
$range = app('steam')->balanceInRange($account, $start, $end, $currency);
$previous = array_values($range)[0];
while ($end >= $current) {
$theDate = $current->format('Y-m-d');
$balance = $range[$theDate] ?? $previous;
$label = $current->isoFormat($format);
$entries[$label] = (float)$balance;
$previous = $balance;
$current->addDay();
}
}
if ('1W' === $step || '1M' === $step || '1Y' === $step) {
while ($end >= $current) {
$balance = (float)app('steam')->balance($account, $current, $currency);
$label = app('navigation')->periodShow($current, $step);
$entries[$label] = $balance;
$current = app('navigation')->addPeriod($current, $step, 0);
}
}
$result['entries'] = $entries;
return $result;
}
/** /**
* Shows the balances for a given set of dates and accounts. * Shows the balances for a given set of dates and accounts.
* *
@@ -571,52 +619,4 @@ class AccountController extends Controller
return response()->json($data); return response()->json($data);
} }
/**
* @param Carbon $start
* @param Carbon $end
* @param Account $account
* @param TransactionCurrency $currency
*
* @return array
* @throws FireflyException
* @throws JsonException
*/
private function periodByCurrency(Carbon $start, Carbon $end, Account $account, TransactionCurrency $currency): array
{
$locale = app('steam')->getLocale();
$step = $this->calculateStep($start, $end);
$result = [
'label' => sprintf('%s (%s)', $account->name, $currency->symbol),
'currency_symbol' => $currency->symbol,
'currency_code' => $currency->code,
];
$entries = [];
$current = clone $start;
if ('1D' === $step) {
// per day the entire period, balance for every day.
$format = (string)trans('config.month_and_day_js', [], $locale);
$range = app('steam')->balanceInRange($account, $start, $end, $currency);
$previous = array_values($range)[0];
while ($end >= $current) {
$theDate = $current->format('Y-m-d');
$balance = $range[$theDate] ?? $previous;
$label = $current->isoFormat($format);
$entries[$label] = (float)$balance;
$previous = $balance;
$current->addDay();
}
}
if ('1W' === $step || '1M' === $step || '1Y' === $step) {
while ($end >= $current) {
$balance = (float)app('steam')->balance($account, $current, $currency);
$label = app('navigation')->periodShow($current, $step);
$entries[$label] = $balance;
$current = app('navigation')->addPeriod($current, $step, 0);
}
}
$result['entries'] = $entries;
return $result;
}
} }

View File

@@ -226,6 +226,29 @@ class BudgetReportController extends Controller
return response()->json($data); return response()->json($data);
} }
/**
* @param Carbon $start
* @param Carbon $end
*
* @return array
*/
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
$preferredRange = app('navigation')->preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = app('navigation')->endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
/** /**
* Chart that groups expenses by the account. * Chart that groups expenses by the account.
* *
@@ -262,27 +285,4 @@ class BudgetReportController extends Controller
return response()->json($data); return response()->json($data);
} }
/**
* @param Carbon $start
* @param Carbon $end
*
* @return array
*/
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
$preferredRange = app('navigation')->preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = app('navigation')->endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
} }

View File

@@ -102,6 +102,14 @@ class CategoryController extends Controller
return response()->json($data); return response()->json($data);
} }
/**
* @return Carbon
*/
private function getDate(): Carbon
{
return today(config('app.timezone'));
}
/** /**
* Shows the category chart on the front page. * Shows the category chart on the front page.
* TODO test method for category refactor. * TODO test method for category refactor.
@@ -158,81 +166,6 @@ class CategoryController extends Controller
return response()->json($data); return response()->json($data);
} }
/**
* Chart for period for transactions without a category.
* TODO test me.
*
* @param Collection $accounts
* @param Carbon $start
* @param Carbon $end
*
* @return JsonResponse
*/
public function reportPeriodNoCategory(Collection $accounts, Carbon $start, Carbon $end): JsonResponse
{
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty('chart.category.period.no-cat');
$cache->addProperty($accounts->pluck('id')->toArray());
if ($cache->has()) {
return response()->json($cache->get());
}
$data = $this->reportPeriodChart($accounts, $start, $end, null);
$cache->store($data);
return response()->json($data);
}
/**
* Chart for a specific period.
* TODO test me, for category refactor.
*
* @param Category $category
* @param Carbon $date
*
* @return JsonResponse
* @throws FireflyException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function specificPeriod(Category $category, Carbon $date): JsonResponse
{
$range = app('navigation')->getViewRange(false);
$start = app('navigation')->startOfPeriod($date, $range);
$end = session()->get('end');
if ($end < $start) {
[$end, $start] = [$start, $end];
}
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty($category->id);
$cache->addProperty('chart.category.period-chart');
if ($cache->has()) {
return response()->json($cache->get());
}
/** @var WholePeriodChartGenerator $chartGenerator */
$chartGenerator = app(WholePeriodChartGenerator::class);
$chartData = $chartGenerator->generate($category, $start, $end);
$data = $this->generator->multiSet($chartData);
$cache->store($data);
return response()->json($data);
}
/**
* @return Carbon
*/
private function getDate(): Carbon
{
return today(config('app.timezone'));
}
/** /**
* Generate report chart for either with or without category. * Generate report chart for either with or without category.
* *
@@ -318,4 +251,71 @@ class CategoryController extends Controller
return $this->generator->multiSet($chartData); return $this->generator->multiSet($chartData);
} }
/**
* Chart for period for transactions without a category.
* TODO test me.
*
* @param Collection $accounts
* @param Carbon $start
* @param Carbon $end
*
* @return JsonResponse
*/
public function reportPeriodNoCategory(Collection $accounts, Carbon $start, Carbon $end): JsonResponse
{
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty('chart.category.period.no-cat');
$cache->addProperty($accounts->pluck('id')->toArray());
if ($cache->has()) {
return response()->json($cache->get());
}
$data = $this->reportPeriodChart($accounts, $start, $end, null);
$cache->store($data);
return response()->json($data);
}
/**
* Chart for a specific period.
* TODO test me, for category refactor.
*
* @param Category $category
* @param Carbon $date
*
* @return JsonResponse
* @throws FireflyException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function specificPeriod(Category $category, Carbon $date): JsonResponse
{
$range = app('navigation')->getViewRange(false);
$start = app('navigation')->startOfPeriod($date, $range);
$end = session()->get('end');
if ($end < $start) {
[$end, $start] = [$start, $end];
}
$cache = new CacheProperties();
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty($category->id);
$cache->addProperty('chart.category.period-chart');
if ($cache->has()) {
return response()->json($cache->get());
}
/** @var WholePeriodChartGenerator $chartGenerator */
$chartGenerator = app(WholePeriodChartGenerator::class);
$chartData = $chartGenerator->generate($category, $start, $end);
$data = $this->generator->multiSet($chartData);
$cache->store($data);
return response()->json($data);
}
} }

View File

@@ -319,6 +319,31 @@ class CategoryReportController extends Controller
return response()->json($data); return response()->json($data);
} }
/**
* TODO duplicate function
*
* @param Carbon $start
* @param Carbon $end
*
* @return array
*/
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
$preferredRange = app('navigation')->preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = app('navigation')->endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
/** /**
* @param Collection $accounts * @param Collection $accounts
* @param Collection $categories * @param Collection $categories
@@ -390,29 +415,4 @@ class CategoryReportController extends Controller
return response()->json($data); return response()->json($data);
} }
/**
* TODO duplicate function
*
* @param Carbon $start
* @param Carbon $end
*
* @return array
*/
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
$preferredRange = app('navigation')->preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = app('navigation')->endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
} }

View File

@@ -247,6 +247,56 @@ class DoubleReportController extends Controller
return response()->json($data); return response()->json($data);
} }
/**
* TODO duplicate function
*
* @param Collection $accounts
* @param int $id
* @param string $name
* @param null|string $iban
*
* @return string
*/
private function getCounterpartName(Collection $accounts, int $id, string $name, ?string $iban): string
{
/** @var Account $account */
foreach ($accounts as $account) {
if ($account->name === $name && $account->id !== $id) {
return $account->name;
}
if (null !== $account->iban && $account->iban === $iban && $account->id !== $id) {
return $account->iban;
}
}
return $name;
}
/**
* TODO duplicate function
*
* @param Carbon $start
* @param Carbon $end
*
* @return array
*/
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
$preferredRange = app('navigation')->preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = app('navigation')->endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
/** /**
* @param Collection $accounts * @param Collection $accounts
* @param Collection $others * @param Collection $others
@@ -366,54 +416,4 @@ class DoubleReportController extends Controller
return response()->json($data); return response()->json($data);
} }
/**
* TODO duplicate function
*
* @param Collection $accounts
* @param int $id
* @param string $name
* @param null|string $iban
*
* @return string
*/
private function getCounterpartName(Collection $accounts, int $id, string $name, ?string $iban): string
{
/** @var Account $account */
foreach ($accounts as $account) {
if ($account->name === $name && $account->id !== $id) {
return $account->name;
}
if (null !== $account->iban && $account->iban === $iban && $account->id !== $id) {
return $account->iban;
}
}
return $name;
}
/**
* TODO duplicate function
*
* @param Carbon $start
* @param Carbon $end
*
* @return array
*/
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
$preferredRange = app('navigation')->preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = app('navigation')->endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
} }

View File

@@ -324,6 +324,31 @@ class TagReportController extends Controller
return response()->json($data); return response()->json($data);
} }
/**
* TODO duplicate function
*
* @param Carbon $start
* @param Carbon $end
*
* @return array
*/
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
$preferredRange = app('navigation')->preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = app('navigation')->endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
/** /**
* @param Collection $accounts * @param Collection $accounts
* @param Collection $tags * @param Collection $tags
@@ -463,29 +488,4 @@ class TagReportController extends Controller
return response()->json($data); return response()->json($data);
} }
/**
* TODO duplicate function
*
* @param Carbon $start
* @param Carbon $end
*
* @return array
*/
private function makeEntries(Carbon $start, Carbon $end): array
{
$return = [];
$format = app('navigation')->preferredCarbonLocalizedFormat($start, $end);
$preferredRange = app('navigation')->preferredRangeFormat($start, $end);
$currentStart = clone $start;
while ($currentStart <= $end) {
$currentEnd = app('navigation')->endOfPeriod($currentStart, $preferredRange);
$key = $currentStart->isoFormat($format);
$return[$key] = '0';
$currentStart = clone $currentEnd;
$currentStart->addDay()->startOfDay();
}
return $return;
}
} }

View File

@@ -169,6 +169,46 @@ class ReconcileController extends Controller
return response()->json($return); return response()->json($return);
} }
/**
* @param Account $account
* @param TransactionCurrency $currency
* @param array $journal
* @param string $amount
*
* @return string
*/
private function processJournal(Account $account, TransactionCurrency $currency, array $journal, string $amount): string
{
$toAdd = '0';
Log::debug(sprintf('User submitted %s #%d: "%s"', $journal['transaction_type_type'], $journal['transaction_journal_id'], $journal['description']));
// not much magic below we need to cover using tests.
if ($account->id === $journal['source_account_id']) {
if ($currency->id === $journal['currency_id']) {
$toAdd = $journal['amount'];
}
if (null !== $journal['foreign_currency_id'] && $journal['foreign_currency_id'] === $currency->id) {
$toAdd = $journal['foreign_amount'];
}
}
if ($account->id === $journal['destination_account_id']) {
if ($currency->id === $journal['currency_id']) {
$toAdd = bcmul($journal['amount'], '-1');
}
if (null !== $journal['foreign_currency_id'] && $journal['foreign_currency_id'] === $currency->id) {
$toAdd = bcmul($journal['foreign_amount'], '-1');
}
}
Log::debug(sprintf('Going to add %s to %s', $toAdd, $amount));
$amount = bcadd($amount, $toAdd);
Log::debug(sprintf('Result is %s', $amount));
return $amount;
}
/** /**
* Returns a list of transactions in a modal. * Returns a list of transactions in a modal.
* *
@@ -225,46 +265,6 @@ class ReconcileController extends Controller
return response()->json(['html' => $html, 'startBalance' => $startBalance, 'endBalance' => $endBalance]); return response()->json(['html' => $html, 'startBalance' => $startBalance, 'endBalance' => $endBalance]);
} }
/**
* @param Account $account
* @param TransactionCurrency $currency
* @param array $journal
* @param string $amount
*
* @return string
*/
private function processJournal(Account $account, TransactionCurrency $currency, array $journal, string $amount): string
{
$toAdd = '0';
Log::debug(sprintf('User submitted %s #%d: "%s"', $journal['transaction_type_type'], $journal['transaction_journal_id'], $journal['description']));
// not much magic below we need to cover using tests.
if ($account->id === $journal['source_account_id']) {
if ($currency->id === $journal['currency_id']) {
$toAdd = $journal['amount'];
}
if (null !== $journal['foreign_currency_id'] && $journal['foreign_currency_id'] === $currency->id) {
$toAdd = $journal['foreign_amount'];
}
}
if ($account->id === $journal['destination_account_id']) {
if ($currency->id === $journal['currency_id']) {
$toAdd = bcmul($journal['amount'], '-1');
}
if (null !== $journal['foreign_currency_id'] && $journal['foreign_currency_id'] === $currency->id) {
$toAdd = bcmul($journal['foreign_amount'], '-1');
}
}
Log::debug(sprintf('Going to add %s to %s', $toAdd, $amount));
$amount = bcadd($amount, $toAdd);
Log::debug(sprintf('Result is %s', $amount));
return $amount;
}
/** /**
* "fix" amounts to make it easier on the reconciliation overview: * "fix" amounts to make it easier on the reconciliation overview:
* *

View File

@@ -144,29 +144,6 @@ class IndexController extends Controller
return view('piggy-banks.index', compact('piggyBanks', 'accounts')); return view('piggy-banks.index', compact('piggyBanks', 'accounts'));
} }
/**
* Set the order of a piggy bank.
*
* @param Request $request
* @param PiggyBank $piggyBank
*
* @return JsonResponse
*/
public function setOrder(Request $request, PiggyBank $piggyBank): JsonResponse
{
$objectGroupTitle = (string)$request->get('objectGroupTitle');
$newOrder = (int)$request->get('order');
$this->piggyRepos->setOrder($piggyBank, $newOrder);
if ('' !== $objectGroupTitle) {
$this->piggyRepos->setObjectGroup($piggyBank, $objectGroupTitle);
}
if ('' === $objectGroupTitle) {
$this->piggyRepos->removeObjectGroup($piggyBank);
}
return response()->json(['data' => 'OK']);
}
/** /**
* @param array $piggyBanks * @param array $piggyBanks
* *
@@ -206,4 +183,27 @@ class IndexController extends Controller
return $piggyBanks; return $piggyBanks;
} }
/**
* Set the order of a piggy bank.
*
* @param Request $request
* @param PiggyBank $piggyBank
*
* @return JsonResponse
*/
public function setOrder(Request $request, PiggyBank $piggyBank): JsonResponse
{
$objectGroupTitle = (string)$request->get('objectGroupTitle');
$newOrder = (int)$request->get('order');
$this->piggyRepos->setOrder($piggyBank, $newOrder);
if ('' !== $objectGroupTitle) {
$this->piggyRepos->setObjectGroup($piggyBank, $objectGroupTitle);
}
if ('' === $objectGroupTitle) {
$this->piggyRepos->removeObjectGroup($piggyBank);
}
return response()->json(['data' => 'OK']);
}
} }

View File

@@ -95,51 +95,6 @@ class ProfileController extends Controller
$this->middleware(IsDemoUser::class)->except(['index']); $this->middleware(IsDemoUser::class)->except(['index']);
} }
/**
* Change your email address.
*
* @param Request $request
*
* @return Factory|RedirectResponse|View
*/
public function changeEmail(Request $request): Factory|RedirectResponse|View
{
if (!$this->internalAuth) {
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
return redirect(route('profile.index'));
}
$title = auth()->user()->email;
$email = auth()->user()->email;
$subTitle = (string)trans('firefly.change_your_email');
$subTitleIcon = 'fa-envelope';
return view('profile.change-email', compact('title', 'subTitle', 'subTitleIcon', 'email'));
}
/**
* Change your password.
*
* @param Request $request
*
* @return Factory|RedirectResponse|Redirector|View
*/
public function changePassword(Request $request)
{
if (!$this->internalAuth) {
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
return redirect(route('profile.index'));
}
$title = auth()->user()->email;
$subTitle = (string)trans('firefly.change_your_password');
$subTitleIcon = 'fa-key';
return view('profile.change-password', compact('title', 'subTitle', 'subTitleIcon'));
}
/** /**
* View that generates a 2FA code for the user. * View that generates a 2FA code for the user.
* *
@@ -442,6 +397,29 @@ class ProfileController extends Controller
return redirect(route('index')); return redirect(route('index'));
} }
/**
* Change your email address.
*
* @param Request $request
*
* @return Factory|RedirectResponse|View
*/
public function changeEmail(Request $request): Factory | RedirectResponse | View
{
if (!$this->internalAuth) {
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
return redirect(route('profile.index'));
}
$title = auth()->user()->email;
$email = auth()->user()->email;
$subTitle = (string)trans('firefly.change_your_email');
$subTitleIcon = 'fa-envelope';
return view('profile.change-email', compact('title', 'subTitle', 'subTitleIcon', 'email'));
}
/** /**
* Submit change password form. * Submit change password form.
* *
@@ -477,6 +455,28 @@ class ProfileController extends Controller
return redirect(route('profile.index')); return redirect(route('profile.index'));
} }
/**
* Change your password.
*
* @param Request $request
*
* @return Factory|RedirectResponse|Redirector|View
*/
public function changePassword(Request $request)
{
if (!$this->internalAuth) {
$request->session()->flash('error', trans('firefly.external_user_mgt_disabled'));
return redirect(route('profile.index'));
}
$title = auth()->user()->email;
$subTitle = (string)trans('firefly.change_your_password');
$subTitleIcon = 'fa-key';
return view('profile.change-password', compact('title', 'subTitle', 'subTitleIcon'));
}
/** /**
* Submit 2FA for the first time. * Submit 2FA for the first time.
* *
@@ -527,6 +527,51 @@ class ProfileController extends Controller
return redirect(route('profile.index')); return redirect(route('profile.index'));
} }
/**
* TODO duplicate code.
*
* @param string $mfaCode
*
* @throws FireflyException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function addToMFAHistory(string $mfaCode): void
{
/** @var array $mfaHistory */
$mfaHistory = app('preferences')->get('mfa_history', [])->data;
$entry = [
'time' => time(),
'code' => $mfaCode,
];
$mfaHistory[] = $entry;
app('preferences')->set('mfa_history', $mfaHistory);
$this->filterMFAHistory();
}
/**
* Remove old entries from the preferences array.
*/
private function filterMFAHistory(): void
{
/** @var array $mfaHistory */
$mfaHistory = app('preferences')->get('mfa_history', [])->data;
$newHistory = [];
$now = time();
foreach ($mfaHistory as $entry) {
$time = $entry['time'];
$code = $entry['code'];
if ($now - $time <= 300) {
$newHistory[] = [
'time' => $time,
'code' => $code,
];
}
}
app('preferences')->set('mfa_history', $newHistory);
}
/** /**
* Submit delete account. * Submit delete account.
* *
@@ -666,49 +711,4 @@ class ProfileController extends Controller
return redirect(route('login')); return redirect(route('login'));
} }
/**
* TODO duplicate code.
*
* @param string $mfaCode
*
* @throws FireflyException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function addToMFAHistory(string $mfaCode): void
{
/** @var array $mfaHistory */
$mfaHistory = app('preferences')->get('mfa_history', [])->data;
$entry = [
'time' => time(),
'code' => $mfaCode,
];
$mfaHistory[] = $entry;
app('preferences')->set('mfa_history', $mfaHistory);
$this->filterMFAHistory();
}
/**
* Remove old entries from the preferences array.
*/
private function filterMFAHistory(): void
{
/** @var array $mfaHistory */
$mfaHistory = app('preferences')->get('mfa_history', [])->data;
$newHistory = [];
$now = time();
foreach ($mfaHistory as $entry) {
$time = $entry['time'];
$code = $entry['code'];
if ($now - $time <= 300) {
$newHistory[] = [
'time' => $time,
'code' => $code,
];
}
}
app('preferences')->set('mfa_history', $newHistory);
}
} }

View File

@@ -295,6 +295,31 @@ class DoubleController extends Controller
return view('reports.double.partials.accounts', compact('sums', 'report')); return view('reports.double.partials.accounts', compact('sums', 'report'));
} }
/**
* TODO this method is duplicated.
*
* @param Collection $accounts
* @param int $id
* @param string $name
* @param string|null $iban
*
* @return string
*/
private function getCounterpartName(Collection $accounts, int $id, string $name, ?string $iban): string
{
/** @var Account $account */
foreach ($accounts as $account) {
if ($account->name === $name && $account->id !== $id) {
return $account->name;
}
if (null !== $account->iban && $account->iban === $iban && $account->id !== $id) {
return $account->iban;
}
}
return $name;
}
/** /**
* @param Collection $accounts * @param Collection $accounts
* @param Collection $double * @param Collection $double
@@ -496,29 +521,4 @@ class DoubleController extends Controller
return $result; return $result;
} }
/**
* TODO this method is duplicated.
*
* @param Collection $accounts
* @param int $id
* @param string $name
* @param string|null $iban
*
* @return string
*/
private function getCounterpartName(Collection $accounts, int $id, string $name, ?string $iban): string
{
/** @var Account $account */
foreach ($accounts as $account) {
if ($account->name === $name && $account->id !== $id) {
return $account->name;
}
if (null !== $account->iban && $account->iban === $iban && $account->id !== $id) {
return $account->iban;
}
}
return $name;
}
} }

View File

@@ -139,32 +139,6 @@ class EditController extends Controller
return view('rules.rule.edit', compact('rule', 'subTitle', 'primaryTrigger', 'oldTriggers', 'oldActions', 'triggerCount', 'actionCount')); return view('rules.rule.edit', compact('rule', 'subTitle', 'primaryTrigger', 'oldTriggers', 'oldActions', 'triggerCount', 'actionCount'));
} }
/**
* Update the rule.
*
* @param RuleFormRequest $request
* @param Rule $rule
*
* @return RedirectResponse|Redirector
*/
public function update(RuleFormRequest $request, Rule $rule)
{
$data = $request->getRuleData();
$this->ruleRepos->update($rule, $data);
session()->flash('success', (string)trans('firefly.updated_rule', ['title' => $rule->title]));
app('preferences')->mark();
$redirect = redirect($this->getPreviousUrl('rules.edit.url'));
if (1 === (int)$request->get('return_to_edit')) {
session()->put('rules.edit.fromUpdate', true);
$redirect = redirect(route('rules.edit', [$rule->id]))->withInput(['return_to_edit' => 1]);
}
return $redirect;
}
/** /**
* @param array $submittedOperators * @param array $submittedOperators
* *
@@ -208,4 +182,30 @@ class EditController extends Controller
return $renderedEntries; return $renderedEntries;
} }
/**
* Update the rule.
*
* @param RuleFormRequest $request
* @param Rule $rule
*
* @return RedirectResponse|Redirector
*/
public function update(RuleFormRequest $request, Rule $rule)
{
$data = $request->getRuleData();
$this->ruleRepos->update($rule, $data);
session()->flash('success', (string)trans('firefly.updated_rule', ['title' => $rule->title]));
app('preferences')->mark();
$redirect = redirect($this->getPreviousUrl('rules.edit.url'));
if (1 === (int)$request->get('return_to_edit')) {
session()->put('rules.edit.fromUpdate', true);
$redirect = redirect(route('rules.edit', [$rule->id]))->withInput(['return_to_edit' => 1]);
}
return $redirect;
}
} }

Some files were not shown because too many files have changed in this diff Show More