Compare commits

...

52 Commits

Author SHA1 Message Date
github-actions[bot]
e74163a7ec Merge pull request #11663 from firefly-iii/release-1770445570
🤖 Automatically merge the PR into the develop branch.
2026-02-07 07:26:17 +01:00
JC5
c60094d231 🤖 Auto commit for release 'develop' on 2026-02-07 2026-02-07 07:26:10 +01:00
James Cole
39d46d469c Fix query parser logging. 2026-02-07 06:53:12 +01:00
github-actions[bot]
6caea5ffa3 Merge pull request #11662 from firefly-iii/release-1770442761
🤖 Automatically merge the PR into the develop branch.
2026-02-07 06:39:28 +01:00
JC5
4024f76a51 🤖 Auto commit for release 'develop' on 2026-02-07 2026-02-07 06:39:21 +01:00
James Cole
de84946371 Expand changelog. 2026-02-07 06:33:30 +01:00
James Cole
6d4aca54de Fix #11246 2026-02-07 06:32:11 +01:00
James Cole
256262b2ba Fix #11657 2026-02-07 06:16:23 +01:00
James Cole
fb035ba594 Fix #11660 2026-02-07 06:09:41 +01:00
James Cole
20776949a6 Clean up changelog. 2026-02-06 18:32:55 +01:00
github-actions[bot]
ad5a8a2934 Merge pull request #11656 from firefly-iii/release-1770398774
🤖 Automatically merge the PR into the develop branch.
2026-02-06 18:26:25 +01:00
JC5
e37ef69491 🤖 Auto commit for release 'develop' on 2026-02-06 2026-02-06 18:26:14 +01:00
James Cole
df8a406c58 Fix issue with email change. 2026-02-06 18:12:52 +01:00
James Cole
88d3e01065 Add events for opening balance. 2026-02-06 18:10:41 +01:00
James Cole
7a1c32f1aa Expand changelog. 2026-02-06 15:58:45 +01:00
James Cole
54df0d44f7 Clean up events 2026-02-06 15:47:34 +01:00
James Cole
1f7775032b Fix budgeted amounts. 2026-02-06 15:38:32 +01:00
github-actions[bot]
8cfe1e8047 Merge pull request #11655 from firefly-iii/release-1770383285
🤖 Automatically merge the PR into the develop branch.
2026-02-06 14:08:15 +01:00
JC5
f0be634829 🤖 Auto commit for release 'develop' on 2026-02-06 2026-02-06 14:08:05 +01:00
James Cole
485e1138f8 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop 2026-02-06 14:03:26 +01:00
James Cole
9abf08b3be Fix view range. 2026-02-06 14:02:37 +01:00
github-actions[bot]
f08943b926 Merge pull request #11654 from firefly-iii/release-1770382517
🤖 Automatically merge the PR into the develop branch.
2026-02-06 13:55:25 +01:00
JC5
2de9926db8 🤖 Auto commit for release 'develop' on 2026-02-06 2026-02-06 13:55:17 +01:00
James Cole
b4d01d464d Clean up more events. 2026-02-06 13:49:50 +01:00
James Cole
229b45c7ad Clean up events for budgets. 2026-02-06 13:47:18 +01:00
James Cole
8e89c5af62 Clean up budget limit events. 2026-02-06 08:33:10 +01:00
James Cole
264fec7a6a Clean up budget limit events. 2026-02-06 08:20:04 +01:00
James Cole
d4b1d097fe Clean up events, although handler is still a little sloppy. 2026-02-06 07:23:31 +01:00
James Cole
bbd6acb824 Create events and respond to budget limit changes. 2026-02-06 06:24:57 +01:00
James Cole
4e7d12f06b Make sure webhook messages are sent. 2026-02-06 06:04:52 +01:00
James Cole
9811583379 Merge branch 'develop' of github.com:firefly-iii/firefly-iii into develop
# Conflicts:
#	app/Http/Controllers/Transaction/DeleteController.php
2026-02-06 05:59:34 +01:00
James Cole
0063cab690 Clean up config. 2026-02-06 05:59:03 +01:00
github-actions[bot]
d3add7c92b Merge pull request #11647 from firefly-iii/release-1770268490
🤖 Automatically merge the PR into the develop branch.
2026-02-05 06:14:57 +01:00
JC5
a491e4921f 🤖 Auto commit for release 'develop' on 2026-02-05 2026-02-05 06:14:50 +01:00
James Cole
171bc03668 Fix running balance events. 2026-02-05 06:10:25 +01:00
James Cole
dd5476bfc7 Clean up events and filters. 2026-02-05 06:02:32 +01:00
James Cole
bc0769358d Clean up update handlers. 2026-02-05 05:51:44 +01:00
James Cole
ccf33f1db6 Also include delete event in new event triggers. 2026-02-05 05:47:37 +01:00
github-actions[bot]
35f611b3f2 Merge pull request #11645 from firefly-iii/release-1770234250
🤖 Automatically merge the PR into the develop branch.
2026-02-04 20:44:18 +01:00
JC5
e5d394533c 🤖 Auto commit for release 'develop' on 2026-02-04 2026-02-04 20:44:10 +01:00
James Cole
831d39a41e Catch missing nonce 2026-02-04 20:39:54 +01:00
James Cole
2920a9b9e3 Fix call. 2026-02-04 20:39:01 +01:00
James Cole
5c8204e963 Unify more event handlers. 2026-02-04 20:29:28 +01:00
James Cole
d25283f193 Clean up processing for group. 2026-02-04 20:17:47 +01:00
github-actions[bot]
20986e6426 Merge pull request #11644 from firefly-iii/release-1770218546
🤖 Automatically merge the PR into the develop branch.
2026-02-04 16:22:34 +01:00
JC5
9cd0ebe37e 🤖 Auto commit for release 'develop' on 2026-02-04 2026-02-04 16:22:26 +01:00
Sander Dorigo
9c2b83a971 Update event handlers 2026-02-04 16:16:27 +01:00
Sander Dorigo
e1d32da409 New event handler object 2026-02-04 08:29:09 +01:00
Sander Dorigo
c51df8cd83 Move events to service and repos 2026-02-04 08:18:35 +01:00
github-actions[bot]
9f016aed16 Merge pull request #11643 from firefly-iii/release-1770188788
🤖 Automatically merge the PR into the develop branch.
2026-02-04 08:06:37 +01:00
JC5
27df5ea800 🤖 Auto commit for release 'develop' on 2026-02-04 2026-02-04 08:06:28 +01:00
Sander Dorigo
2d7cdd36f0 Fix null pointer 2026-02-04 08:01:47 +01:00
357 changed files with 15192 additions and 14635 deletions

View File

@@ -31,7 +31,6 @@ use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Support\Debug\Timer;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Api\AccountFilter;
@@ -80,7 +79,7 @@ class AccountController extends Controller
*/
public function accounts(AutocompleteApiRequest $request): JsonResponse
{
Log::debug('Before All.');
// Log::debug('Before All.');
['types' => $types, 'query' => $query, 'date' => $date, 'limit' => $limit] = $request->attributes->all();
$date ??= today(config('app.timezone'));
@@ -89,8 +88,6 @@ class AccountController extends Controller
$date->endOfDay();
$return = [];
$timer = Timer::getInstance();
$timer->start(sprintf('AC accounts "%s"', $query));
$result = $this->repository->searchAccount((string) $query, $types, $limit);
$allBalances = Steam::accountsBalancesOptimized($result, $date, $this->primaryCurrency, $this->convertToPrimary);
@@ -136,7 +133,6 @@ class AccountController extends Controller
return $posA - $posB;
});
$timer->stop(sprintf('AC accounts "%s"', $query));
return response()->api($return);
}

View File

@@ -104,6 +104,48 @@ class BudgetController extends Controller
return response()->json($this->clean($data));
}
private function filterLimit(int $currencyId, Collection $limits): ?BudgetLimit
{
$amount = '0';
$limit = null;
$converter = new ExchangeRateConverter();
/** @var BudgetLimit $current */
foreach ($limits as $current) {
if ($this->convertToPrimary) {
if ($current->transaction_currency_id === $this->primaryCurrency->id) {
// simply add it.
$amount = bcadd($amount, (string) $current->amount);
Log::debug(sprintf('Set amount in limit to %s', $amount));
}
if ($current->transaction_currency_id !== $this->primaryCurrency->id) {
// convert and then add it.
$converted = $converter->convert($current->transactionCurrency, $this->primaryCurrency, $current->start_date, $current->amount);
$amount = bcadd($amount, $converted);
Log::debug(sprintf(
'Budgeted in limit #%d: %s %s, converted to %s %s',
$current->id,
$current->transactionCurrency->code,
$current->amount,
$this->primaryCurrency->code,
$converted
));
Log::debug(sprintf('Set amount in limit to %s', $amount));
}
}
if ($current->transaction_currency_id === $currencyId) {
$limit = $current;
}
}
if (null !== $limit && $this->convertToPrimary) {
// convert and add all amounts.
$limit->amount = Steam::positive($amount);
Log::debug(sprintf('Final amount in limit with converted amount %s', $limit->amount));
}
return $limit;
}
/**
* @throws FireflyException
*/
@@ -250,46 +292,4 @@ class BudgetController extends Controller
return $return;
}
private function filterLimit(int $currencyId, Collection $limits): ?BudgetLimit
{
$amount = '0';
$limit = null;
$converter = new ExchangeRateConverter();
/** @var BudgetLimit $current */
foreach ($limits as $current) {
if ($this->convertToPrimary) {
if ($current->transaction_currency_id === $this->primaryCurrency->id) {
// simply add it.
$amount = bcadd($amount, (string) $current->amount);
Log::debug(sprintf('Set amount in limit to %s', $amount));
}
if ($current->transaction_currency_id !== $this->primaryCurrency->id) {
// convert and then add it.
$converted = $converter->convert($current->transactionCurrency, $this->primaryCurrency, $current->start_date, $current->amount);
$amount = bcadd($amount, $converted);
Log::debug(sprintf(
'Budgeted in limit #%d: %s %s, converted to %s %s',
$current->id,
$current->transactionCurrency->code,
$current->amount,
$this->primaryCurrency->code,
$converted
));
Log::debug(sprintf('Set amount in limit to %s', $amount));
}
}
if ($current->transaction_currency_id === $currencyId) {
$limit = $current;
}
}
if (null !== $limit && $this->convertToPrimary) {
// convert and add all amounts.
$limit->amount = Steam::positive($amount);
Log::debug(sprintf('Final amount in limit with converted amount %s', $limit->amount));
}
return $limit;
}
}

View File

@@ -98,6 +98,77 @@ abstract class Controller extends BaseController
});
}
/**
* Method to help build URL's.
*/
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);
}
final protected function getManager(): Manager
{
// create some objects:
$manager = new Manager();
$baseUrl = request()->getSchemeAndHttpHost().'/api/v1';
$manager->setSerializer(new JsonApiSerializer($baseUrl));
return $manager;
}
final protected function jsonApiList(string $key, LengthAwarePaginator $paginator, AbstractTransformer $transformer): array
{
$manager = new Manager();
$baseUrl = sprintf('%s/api/v1/', request()->getSchemeAndHttpHost());
// TODO add stuff to path?
$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.
// $objects = $transformer->collectMetaData($objects);
$paginator->setCollection($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 array<int, mixed>|Model $object
*/
final protected function jsonApiObject(string $key, array|Model $object, AbstractTransformer $transformer): array
{
// create some objects:
$manager = new Manager();
$baseUrl = sprintf('%s/api/v1', request()->getSchemeAndHttpHost());
$manager->setSerializer(new JsonApiSerializer($baseUrl));
$resource = new Item($object, $transformer, $key);
return $manager->createData($resource)->toArray();
}
#[Deprecated(message: <<<'TXT'
use Request classes
Method to grab all parameters from the URL
@@ -170,75 +241,4 @@ abstract class Controller extends BaseController
// return $this->getSortParameters($bag);
}
/**
* Method to help build URL's.
*/
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);
}
final protected function getManager(): Manager
{
// create some objects:
$manager = new Manager();
$baseUrl = request()->getSchemeAndHttpHost().'/api/v1';
$manager->setSerializer(new JsonApiSerializer($baseUrl));
return $manager;
}
final protected function jsonApiList(string $key, LengthAwarePaginator $paginator, AbstractTransformer $transformer): array
{
$manager = new Manager();
$baseUrl = sprintf('%s/api/v1/', request()->getSchemeAndHttpHost());
// TODO add stuff to path?
$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.
// $objects = $transformer->collectMetaData($objects);
$paginator->setCollection($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 array<int, mixed>|Model $object
*/
final protected function jsonApiObject(string $key, array|Model $object, AbstractTransformer $transformer): array
{
// create some objects:
$manager = new Manager();
$baseUrl = sprintf('%s/api/v1', request()->getSchemeAndHttpHost());
$manager->setSerializer(new JsonApiSerializer($baseUrl));
$resource = new Item($object, $transformer, $key);
return $manager->createData($resource)->toArray();
}
}

View File

@@ -137,70 +137,6 @@ class DestroyController extends Controller
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<int, string> $types
*/
@@ -229,6 +165,70 @@ 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<int, string> $types
*/

View File

@@ -72,33 +72,6 @@ class ExportController extends Controller
return $this->returnExport('accounts');
}
/**
* @throws FireflyException
* @throws DatetimeException
*/
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((string) $data[$key]))
;
return $response;
}
/**
* @throws DatetimeException
* @throws FireflyException
@@ -204,4 +177,31 @@ class ExportController extends Controller
return $this->returnExport('transactions');
}
/**
* @throws FireflyException
* @throws DatetimeException
*/
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((string) $data[$key]))
;
return $response;
}
}

View File

@@ -63,13 +63,6 @@ class DestroyController extends Controller
return response()->json([], 204);
}
public function destroySingleById(CurrencyExchangeRate $exchangeRate): JsonResponse
{
$this->repository->deleteRate($exchangeRate);
return response()->json([], 204);
}
public function destroySingleByDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
{
$exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date);
@@ -82,4 +75,11 @@ class DestroyController extends Controller
return response()->json([], 204);
}
public function destroySingleById(CurrencyExchangeRate $exchangeRate): JsonResponse
{
$this->repository->deleteRate($exchangeRate);
return response()->json([], 204);
}
}

View File

@@ -75,14 +75,6 @@ class ShowController extends Controller
return response()->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
public function showSingleById(CurrencyExchangeRate $exchangeRate): JsonResponse
{
$transformer = new ExchangeRateTransformer();
$transformer->setParameters($this->parameters);
return response()->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
public function showSingleByDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
{
$transformer = new ExchangeRateTransformer();
@@ -95,4 +87,12 @@ class ShowController extends Controller
return response()->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
public function showSingleById(CurrencyExchangeRate $exchangeRate): JsonResponse
{
$transformer = new ExchangeRateTransformer();
$transformer->setParameters($this->parameters);
return response()->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
}

View File

@@ -61,6 +61,30 @@ class StoreController extends Controller
});
}
public function store(StoreRequest $request): JsonResponse
{
$date = $request->getDate();
$rate = $request->getRate();
$from = $request->getFromCurrency();
$to = $request->getToCurrency();
// already has rate?
$object = $this->repository->getSpecificRateOnDate($from, $to, $date);
if ($object instanceof CurrencyExchangeRate) {
// just update it, no matter.
$rate = $this->repository->updateExchangeRate($object, $rate, $date);
}
if (!$object instanceof CurrencyExchangeRate) {
// store new
$rate = $this->repository->storeExchangeRate($from, $to, $rate, $date);
}
$transformer = new ExchangeRateTransformer();
$transformer->setParameters($this->parameters);
return response()->api($this->jsonApiObject(self::RESOURCE_KEY, $rate, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
public function storeByCurrencies(StoreByCurrenciesRequest $request, TransactionCurrency $from, TransactionCurrency $to): JsonResponse
{
$data = $request->getAll();
@@ -114,28 +138,4 @@ class StoreController extends Controller
return response()->json($this->jsonApiList(self::RESOURCE_KEY, $paginator, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
public function store(StoreRequest $request): JsonResponse
{
$date = $request->getDate();
$rate = $request->getRate();
$from = $request->getFromCurrency();
$to = $request->getToCurrency();
// already has rate?
$object = $this->repository->getSpecificRateOnDate($from, $to, $date);
if ($object instanceof CurrencyExchangeRate) {
// just update it, no matter.
$rate = $this->repository->updateExchangeRate($object, $rate, $date);
}
if (!$object instanceof CurrencyExchangeRate) {
// store new
$rate = $this->repository->storeExchangeRate($from, $to, $rate, $date);
}
$transformer = new ExchangeRateTransformer();
$transformer->setParameters($this->parameters);
return response()->api($this->jsonApiObject(self::RESOURCE_KEY, $rate, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
}

View File

@@ -57,17 +57,6 @@ class UpdateController extends Controller
});
}
public function updateById(UpdateRequest $request, CurrencyExchangeRate $exchangeRate): JsonResponse
{
$date = $request->getDate();
$rate = $request->getRate();
$exchangeRate = $this->repository->updateExchangeRate($exchangeRate, $rate, $date);
$transformer = new ExchangeRateTransformer();
$transformer->setParameters($this->parameters);
return response()->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
public function updateByDate(UpdateRequest $request, TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
{
$exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date);
@@ -82,4 +71,15 @@ class UpdateController extends Controller
return response()->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
public function updateById(UpdateRequest $request, CurrencyExchangeRate $exchangeRate): JsonResponse
{
$date = $request->getDate();
$rate = $request->getRate();
$exchangeRate = $this->repository->updateExchangeRate($exchangeRate, $rate, $date);
$transformer = new ExchangeRateTransformer();
$transformer->setParameters($this->parameters);
return response()->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer))->header('Content-Type', self::CONTENT_TYPE);
}
}

View File

@@ -25,9 +25,6 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Models\Transaction;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Events\UpdatedAccount;
use FireflyIII\Models\Account;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
@@ -74,31 +71,9 @@ class DestroyController extends Controller
public function destroy(TransactionGroup $transactionGroup): JsonResponse
{
Log::debug(sprintf('Now in %s', __METHOD__));
// grab asset account(s) from group:
$accounts = [];
/** @var TransactionJournal $journal */
foreach ($transactionGroup->transactionJournals as $journal) {
/** @var Transaction $transaction */
foreach ($journal->transactions as $transaction) {
$type = $transaction->account->accountType->type;
// if is valid liability, trigger event!
if (in_array($type, config('firefly.valid_liabilities'), true)) {
$accounts[] = $transaction->account;
}
}
}
$this->groupRepository->destroy($transactionGroup);
Preferences::mark();
/** @var Account $account */
foreach ($accounts as $account) {
Log::debug(sprintf('Now going to trigger updated account event for account #%d', $account->id));
event(new UpdatedAccount($account));
}
return response()->json([], 204);
}

View File

@@ -99,17 +99,6 @@ class ShowController extends Controller
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.
*/
public function showJournal(TransactionJournal $transactionJournal): JsonResponse
{
return $this->show($transactionJournal->transactionGroup);
}
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/transactions/getTransaction
@@ -151,4 +140,15 @@ class ShowController extends Controller
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.
*/
public function showJournal(TransactionJournal $transactionJournal): JsonResponse
{
return $this->show($transactionJournal->transactionGroup);
}
}

View File

@@ -27,14 +27,11 @@ namespace FireflyIII\Api\V1\Controllers\Models\Transaction;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\Transaction\StoreRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Events\Model\TransactionGroup\CreatedSingleTransactionGroup;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Exceptions\DuplicateTransactionException;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface;
use FireflyIII\Rules\IsDuplicateTransaction;
use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Http\Api\TransactionFilter;
use FireflyIII\Support\JsonApi\Enrichments\TransactionGroupEnrichment;
use FireflyIII\Transformers\TransactionGroupTransformer;
@@ -88,9 +85,9 @@ class StoreController extends Controller
public function store(StoreRequest $request): JsonResponse
{
Log::debug('Now in API StoreController::store()');
$data = $request->getAll();
$data['user'] = auth()->user();
$data['user_group'] = $this->userGroup;
$data = $request->getAll();
$data['user'] = auth()->user();
$data['user_group'] = $this->userGroup;
Log::channel('audit')->info('Store new transaction over API.', $data);
@@ -109,22 +106,15 @@ class StoreController extends Controller
throw new ValidationException($validator);
}
Preferences::mark();
$flags = new TransactionGroupEventFlags();
$flags->applyRules = $data['apply_rules'] ?? true;
$flags->fireWebhooks = $data['fire_webhooks'] ?? true;
$flags->batchSubmission = $data['batch_submission'] ?? false;
Log::debug('CreatedSingleTransactionGroup');
event(new CreatedSingleTransactionGroup($transactionGroup, $flags));
$manager = $this->getManager();
$manager = $this->getManager();
/** @var User $admin */
$admin = auth()->user();
$admin = auth()->user();
// use new group collector:
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
$collector = app(GroupCollectorInterface::class);
$collector
->setUser($admin)
->setUserGroup($this->userGroup)
@@ -134,20 +124,20 @@ class StoreController extends Controller
->withAPIInformation()
;
$selectedGroup = $collector->getGroups()->first();
$selectedGroup = $collector->getGroups()->first();
if (null === $selectedGroup) {
throw HttpException::fromStatusCode(410, '200032: Cannot find transaction. Possibly, a rule deleted this transaction after its creation.');
}
// enrich
$enrichment = new TransactionGroupEnrichment();
$enrichment = new TransactionGroupEnrichment();
$enrichment->setUser($admin);
$selectedGroup = $enrichment->enrichSingle($selectedGroup);
$selectedGroup = $enrichment->enrichSingle($selectedGroup);
/** @var TransactionGroupTransformer $transformer */
$transformer = app(TransactionGroupTransformer::class);
$transformer = app(TransactionGroupTransformer::class);
$transformer->setParameters($this->parameters);
$resource = new Item($selectedGroup, $transformer, 'transactions');
$resource = new Item($selectedGroup, $transformer, 'transactions');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}

View File

@@ -27,7 +27,9 @@ namespace FireflyIII\Api\V1\Controllers\Models\Transaction;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Models\Transaction\UpdateRequest;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventObjects;
use FireflyIII\Events\Model\TransactionGroup\UpdatedSingleTransactionGroup;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface;
@@ -75,7 +77,9 @@ class UpdateController extends Controller
Log::debug('Now in update routine for transaction group');
$data = $request->getAll();
$oldHash = $this->groupRepository->getCompareHash($transactionGroup);
$objects = TransactionGroupEventObjects::collectFromTransactionGroup($transactionGroup);
$transactionGroup = $this->groupRepository->update($transactionGroup, $data);
$objects->appendFromTransactionGroup($transactionGroup);
$newHash = $this->groupRepository->getCompareHash($transactionGroup);
$manager = $this->getManager();
@@ -88,7 +92,8 @@ class UpdateController extends Controller
$flags->applyRules = $applyRules;
$flags->fireWebhooks = $fireWebhooks;
$flags->recalculateCredit = $runRecalculations;
event(new UpdatedSingleTransactionGroup($transactionGroup, $flags));
event(new UpdatedSingleTransactionGroup($flags, $objects));
event(new WebhookMessagesRequestSending());
/** @var User $admin */
$admin = auth()->user();

View File

@@ -97,27 +97,6 @@ class UpdateController extends Controller
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}
public function makePrimary(TransactionCurrency $currency): JsonResponse
{
/** @var User $user */
$user = auth()->user();
$this->repository->enable($currency);
$this->repository->makePrimary($currency);
Preferences::mark();
$manager = $this->getManager();
$currency->refreshForUser($user);
/** @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:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/currencies/enableCurrency
@@ -143,6 +122,27 @@ class UpdateController extends Controller
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}
public function makePrimary(TransactionCurrency $currency): JsonResponse
{
/** @var User $user */
$user = auth()->user();
$this->repository->enable($currency);
$this->repository->makePrimary($currency);
Preferences::mark();
$manager = $this->getManager();
$currency->refreshForUser($user);
/** @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:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/currencies/updateCurrency

View File

@@ -112,6 +112,19 @@ class BasicController extends Controller
return response()->json($return);
}
/**
* Check if date is outside session range.
*/
protected function notInDateRange(Carbon $date, Carbon $start, Carbon $end): bool
{ // Validate a preference
if ($start->greaterThanOrEqualTo($date) && $end->greaterThanOrEqualTo($date)) {
return true;
}
// start and end in the past? use $end
return $start->lessThanOrEqualTo($date) && $end->lessThanOrEqualTo($date);
}
private function getBalanceInformation(Carbon $start, Carbon $end): array
{
Log::debug('getBalanceInformation');
@@ -312,145 +325,6 @@ class BasicController extends Controller
return $return;
}
private function getSubscriptionInformation(Carbon $start, Carbon $end): array
{
Log::debug(sprintf('Now in getBillInformation("%s", "%s")', $start->format('Y-m-d'), $end->format('Y-m-d-')));
/*
* Since both this method and the chart use the exact same data, we can suffice
* with calling the one method in the bill repository that will get this amount.
*/
$paidAmount = $this->billRepository->sumPaidInRange($start, $end);
$unpaidAmount = $this->billRepository->sumUnpaidInRange($start, $end);
$currencies = [$this->primaryCurrency->id => $this->primaryCurrency];
if ($this->convertToPrimary) {
$converter = new ExchangeRateConverter();
$newPaidAmount = [[
'id' => $this->primaryCurrency->id,
'name' => $this->primaryCurrency->name,
'symbol' => $this->primaryCurrency->symbol,
'code' => $this->primaryCurrency->code,
'decimal_places' => $this->primaryCurrency->decimal_places,
'sum' => '0',
]];
$newUnpaidAmount = [[
'id' => $this->primaryCurrency->id,
'name' => $this->primaryCurrency->name,
'symbol' => $this->primaryCurrency->symbol,
'code' => $this->primaryCurrency->code,
'decimal_places' => $this->primaryCurrency->decimal_places,
'sum' => '0',
]];
foreach ([$paidAmount, $unpaidAmount] as $index => $array) {
foreach ($array as $item) {
$currencyId = (int) $item['id'];
if (0 === $index) {
// paid amount
if ($currencyId === $this->primaryCurrency->id) {
$newPaidAmount[0]['sum'] = bcadd($newPaidAmount[0]['sum'], (string) $item['sum']);
continue;
}
$currencies[$currencyId] ??= $this->currencyRepos->find($currencyId);
$convertedAmount = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $item['sum']);
$newPaidAmount[0]['sum'] = bcadd($newPaidAmount[0]['sum'], $convertedAmount);
continue;
}
// unpaid amount
if ($currencyId === $this->primaryCurrency->id) {
$newUnpaidAmount[0]['sum'] = bcadd($newUnpaidAmount[0]['sum'], (string) $item['sum']);
continue;
}
$currencies[$currencyId] ??= $this->currencyRepos->find($currencyId);
$convertedAmount = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $item['sum']);
$newUnpaidAmount[0]['sum'] = bcadd($newUnpaidAmount[0]['sum'], $convertedAmount);
}
}
$paidAmount = $newPaidAmount;
$unpaidAmount = $newUnpaidAmount;
}
// var_dump($paidAmount);
// var_dump($unpaidAmount);
// exit;
$return = [];
/**
* @var array $info
*/
foreach ($paidAmount as $info) {
$amount = bcmul((string) $info['sum'], '-1');
$return[] = [
'key' => sprintf('bills-paid-in-%s', $info['code']),
'title' => trans('firefly.box_bill_paid_in_currency', ['currency' => $info['symbol']]),
'monetary_value' => $amount,
'currency_id' => (string) $info['id'],
'currency_code' => $info['code'],
'currency_symbol' => $info['symbol'],
'currency_decimal_places' => $info['decimal_places'],
'value_parsed' => Amount::formatFlat($info['symbol'], $info['decimal_places'], $amount, false),
'local_icon' => 'check',
'sub_title' => '',
];
}
/**
* @var array $info
*/
foreach ($unpaidAmount as $info) {
$amount = bcmul((string) $info['sum'], '-1');
$return[] = [
'key' => sprintf('bills-unpaid-in-%s', $info['code']),
'title' => trans('firefly.box_bill_unpaid_in_currency', ['currency' => $info['symbol']]),
'monetary_value' => $amount,
'currency_id' => (string) $info['id'],
'currency_code' => $info['code'],
'currency_symbol' => $info['symbol'],
'currency_decimal_places' => $info['decimal_places'],
'value_parsed' => Amount::formatFlat($info['symbol'], $info['decimal_places'], $amount, false),
'local_icon' => 'calendar-o',
'sub_title' => '',
];
}
Log::debug(sprintf('Done with getBillInformation("%s", "%s")', $start->format('Y-m-d'), $end->format('Y-m-d-')));
if (0 === count($return)) {
$currency = $this->primaryCurrency;
unset($info, $amount);
$return[] = [
'key' => sprintf('bills-paid-in-%s', $currency->code),
'title' => trans('firefly.box_bill_paid_in_currency', ['currency' => $currency->symbol]),
'monetary_value' => '0',
'currency_id' => (string) $currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'value_parsed' => Amount::formatFlat($currency->symbol, $currency->decimal_places, '0', false),
'local_icon' => 'check',
'sub_title' => '',
];
$return[] = [
'key' => sprintf('bills-unpaid-in-%s', $currency->code),
'title' => trans('firefly.box_bill_unpaid_in_currency', ['currency' => $currency->symbol]),
'monetary_value' => '0',
'currency_id' => (string) $currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'value_parsed' => Amount::formatFlat($currency->symbol, $currency->decimal_places, '0', false),
'local_icon' => 'calendar-o',
'sub_title' => '',
];
}
return $return;
}
/**
* @throws Exception
*/
@@ -650,16 +524,142 @@ class BasicController extends Controller
return $return;
}
/**
* Check if date is outside session range.
*/
protected function notInDateRange(Carbon $date, Carbon $start, Carbon $end): bool
{ // Validate a preference
if ($start->greaterThanOrEqualTo($date) && $end->greaterThanOrEqualTo($date)) {
return true;
private function getSubscriptionInformation(Carbon $start, Carbon $end): array
{
Log::debug(sprintf('Now in getBillInformation("%s", "%s")', $start->format('Y-m-d'), $end->format('Y-m-d-')));
/*
* Since both this method and the chart use the exact same data, we can suffice
* with calling the one method in the bill repository that will get this amount.
*/
$paidAmount = $this->billRepository->sumPaidInRange($start, $end);
$unpaidAmount = $this->billRepository->sumUnpaidInRange($start, $end);
$currencies = [$this->primaryCurrency->id => $this->primaryCurrency];
if ($this->convertToPrimary) {
$converter = new ExchangeRateConverter();
$newPaidAmount = [[
'id' => $this->primaryCurrency->id,
'name' => $this->primaryCurrency->name,
'symbol' => $this->primaryCurrency->symbol,
'code' => $this->primaryCurrency->code,
'decimal_places' => $this->primaryCurrency->decimal_places,
'sum' => '0',
]];
$newUnpaidAmount = [[
'id' => $this->primaryCurrency->id,
'name' => $this->primaryCurrency->name,
'symbol' => $this->primaryCurrency->symbol,
'code' => $this->primaryCurrency->code,
'decimal_places' => $this->primaryCurrency->decimal_places,
'sum' => '0',
]];
foreach ([$paidAmount, $unpaidAmount] as $index => $array) {
foreach ($array as $item) {
$currencyId = (int) $item['id'];
if (0 === $index) {
// paid amount
if ($currencyId === $this->primaryCurrency->id) {
$newPaidAmount[0]['sum'] = bcadd($newPaidAmount[0]['sum'], (string) $item['sum']);
continue;
}
$currencies[$currencyId] ??= $this->currencyRepos->find($currencyId);
$convertedAmount = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $item['sum']);
$newPaidAmount[0]['sum'] = bcadd($newPaidAmount[0]['sum'], $convertedAmount);
continue;
}
// unpaid amount
if ($currencyId === $this->primaryCurrency->id) {
$newUnpaidAmount[0]['sum'] = bcadd($newUnpaidAmount[0]['sum'], (string) $item['sum']);
continue;
}
$currencies[$currencyId] ??= $this->currencyRepos->find($currencyId);
$convertedAmount = $converter->convert($currencies[$currencyId], $this->primaryCurrency, $start, $item['sum']);
$newUnpaidAmount[0]['sum'] = bcadd($newUnpaidAmount[0]['sum'], $convertedAmount);
}
}
$paidAmount = $newPaidAmount;
$unpaidAmount = $newUnpaidAmount;
}
// start and end in the past? use $end
return $start->lessThanOrEqualTo($date) && $end->lessThanOrEqualTo($date);
// var_dump($paidAmount);
// var_dump($unpaidAmount);
// exit;
$return = [];
/**
* @var array $info
*/
foreach ($paidAmount as $info) {
$amount = bcmul((string) $info['sum'], '-1');
$return[] = [
'key' => sprintf('bills-paid-in-%s', $info['code']),
'title' => trans('firefly.box_bill_paid_in_currency', ['currency' => $info['symbol']]),
'monetary_value' => $amount,
'currency_id' => (string) $info['id'],
'currency_code' => $info['code'],
'currency_symbol' => $info['symbol'],
'currency_decimal_places' => $info['decimal_places'],
'value_parsed' => Amount::formatFlat($info['symbol'], $info['decimal_places'], $amount, false),
'local_icon' => 'check',
'sub_title' => '',
];
}
/**
* @var array $info
*/
foreach ($unpaidAmount as $info) {
$amount = bcmul((string) $info['sum'], '-1');
$return[] = [
'key' => sprintf('bills-unpaid-in-%s', $info['code']),
'title' => trans('firefly.box_bill_unpaid_in_currency', ['currency' => $info['symbol']]),
'monetary_value' => $amount,
'currency_id' => (string) $info['id'],
'currency_code' => $info['code'],
'currency_symbol' => $info['symbol'],
'currency_decimal_places' => $info['decimal_places'],
'value_parsed' => Amount::formatFlat($info['symbol'], $info['decimal_places'], $amount, false),
'local_icon' => 'calendar-o',
'sub_title' => '',
];
}
Log::debug(sprintf('Done with getBillInformation("%s", "%s")', $start->format('Y-m-d'), $end->format('Y-m-d-')));
if (0 === count($return)) {
$currency = $this->primaryCurrency;
unset($info, $amount);
$return[] = [
'key' => sprintf('bills-paid-in-%s', $currency->code),
'title' => trans('firefly.box_bill_paid_in_currency', ['currency' => $currency->symbol]),
'monetary_value' => '0',
'currency_id' => (string) $currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'value_parsed' => Amount::formatFlat($currency->symbol, $currency->decimal_places, '0', false),
'local_icon' => 'check',
'sub_title' => '',
];
$return[] = [
'key' => sprintf('bills-unpaid-in-%s', $currency->code),
'title' => trans('firefly.box_bill_unpaid_in_currency', ['currency' => $currency->symbol]),
'monetary_value' => '0',
'currency_id' => (string) $currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
'value_parsed' => Amount::formatFlat($currency->symbol, $currency->decimal_places, '0', false),
'local_icon' => 'calendar-o',
'sub_title' => '',
];
}
return $return;
}
}

View File

@@ -86,37 +86,6 @@ class ConfigurationController extends Controller
return response()->api($return);
}
/**
* Get all config values.
*
* @throws FireflyException
*/
private function getDynamicConfiguration(): array
{
$isDemoSite = FireflyConfig::get('is_demo_site');
$updateCheck = FireflyConfig::get('permission_update_check');
$lastCheck = FireflyConfig::get('last_update_check');
$singleUser = 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,
];
}
private function getStaticConfiguration(): array
{
$list = EitherConfigKey::$static;
$return = [];
foreach ($list as $key) {
$return[$key] = config($key);
}
return $return;
}
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/configuration/getSingleConfiguration
@@ -170,6 +139,37 @@ class ConfigurationController extends Controller
return response()->api(['data' => $data])->header('Content-Type', self::CONTENT_TYPE);
}
/**
* Get all config values.
*
* @throws FireflyException
*/
private function getDynamicConfiguration(): array
{
$isDemoSite = FireflyConfig::get('is_demo_site');
$updateCheck = FireflyConfig::get('permission_update_check');
$lastCheck = FireflyConfig::get('last_update_check');
$singleUser = 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,
];
}
private function getStaticConfiguration(): array
{
$list = EitherConfigKey::$static;
$return = [];
foreach ($list as $key) {
$return[$key] = config($key);
}
return $return;
}
private function getWebhookConfiguration(string $configKey): array
{
switch ($configKey) {

View File

@@ -58,8 +58,6 @@ class UserController extends Controller
});
}
public function finishBatch(): JsonResponse {}
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/users/deleteUser
@@ -85,6 +83,8 @@ class UserController extends Controller
throw new FireflyException('200025: No access to function.');
}
public function finishBatch(): JsonResponse {}
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/users/listUser

View File

@@ -37,9 +37,6 @@ abstract class AggregateFormRequest extends ApiRequest
*/
protected array $requests = [];
/** @return array<array|string> */
abstract protected function getRequests(): array;
#[Override]
public function initialize(
array $query = [],
@@ -53,7 +50,7 @@ abstract class AggregateFormRequest extends ApiRequest
parent::initialize($query, $request, $attributes, $cookies, $files, $server, $content);
// instantiate all subrequests and share current requests' bags with them
Log::debug('Initializing AggregateFormRequest.');
// Log::debug('Initializing AggregateFormRequest.');
/** @var array|string $config */
foreach ($this->getRequests() as $config) {
@@ -62,7 +59,7 @@ abstract class AggregateFormRequest extends ApiRequest
if (!is_a($requestClass, Request::class, true)) {
throw new RuntimeException('getRequests() must return class-strings of subclasses of Request');
}
Log::debug(sprintf('Initializing subrequest %s', $requestClass));
// Log::debug(sprintf('Initializing subrequest %s', $requestClass));
$instance = $this->requests[] = new $requestClass();
$instance->request = $this->request;
@@ -77,7 +74,8 @@ abstract class AggregateFormRequest extends ApiRequest
$instance->handleConfig(is_array($config) ? $config : []);
}
}
Log::debug('Done initializing AggregateFormRequest.');
// Log::debug('Done initializing AggregateFormRequest.');
}
public function rules(): array
@@ -95,9 +93,12 @@ abstract class AggregateFormRequest extends ApiRequest
// register all subrequests' validators
foreach ($this->requests as $request) {
if (method_exists($request, 'withValidator')) {
Log::debug(sprintf('Process withValidator from class %s', $request::class));
// Log::debug(sprintf('Process withValidator from class %s', $request::class));
$request->withValidator($validator);
}
}
}
/** @return array<array|string> */
abstract protected function getRequests(): array;
}

View File

@@ -79,25 +79,6 @@ class GenericRequest extends FormRequest
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);
}
}
}
}
public function getBills(): Collection
{
$this->parseBills();
@@ -105,25 +86,6 @@ class GenericRequest extends FormRequest
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);
}
}
}
}
public function getBudgets(): Collection
{
$this->parseBudgets();
@@ -131,25 +93,6 @@ class GenericRequest extends FormRequest
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);
}
}
}
}
public function getCategories(): Collection
{
$this->parseCategories();
@@ -157,25 +100,6 @@ class GenericRequest extends FormRequest
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);
}
}
}
}
public function getEnd(): Carbon
{
$date = $this->getCarbonDate('end');
@@ -231,6 +155,97 @@ class GenericRequest extends FormRequest
return $this->tags;
}
/**
* The rules that the incoming request must be matched against.
*/
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);
}
}
}
}
private function parseTags(): void
{
if (0 !== $this->tags->count()) {
@@ -249,19 +264,4 @@ class GenericRequest extends FormRequest
}
}
}
/**
* The rules that the incoming request must be matched against.
*/
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

@@ -70,65 +70,6 @@ class StoreRequest extends FormRequest
return ['recurrence' => $recurrence, 'transactions' => $this->getTransactionData(), 'repetitions' => $this->getRepetitionData()];
}
/**
* 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.
*/
private function getTransactionData(): array
{
$return = [];
// transaction data:
/** @var null|array $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.
*/
private function getRepetitionData(): array
{
$return = [];
// repetition data:
/** @var null|array $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.
*/
@@ -190,4 +131,63 @@ class StoreRequest extends FormRequest
Log::channel('audit')->error(sprintf('Validation errors in %s', self::class), $validator->errors()->toArray());
}
}
/**
* Returns the repetition data as it is found in the submitted data.
*/
private function getRepetitionData(): array
{
$return = [];
// repetition data:
/** @var null|array $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.
*/
private function getTransactionData(): array
{
$return = [];
// transaction data:
/** @var null|array $transactions */
$transactions = $this->get('transactions');
if (null === $transactions) {
return [];
}
/** @var array $transaction */
foreach ($transactions as $transaction) {
$return[] = $this->getSingleTransactionData($transaction);
}
return $return;
}
}

View File

@@ -77,70 +77,6 @@ class UpdateRequest extends FormRequest
return $return;
}
/**
* Returns the repetition data as it is found in the submitted data.
*/
private function getRepetitionData(): ?array
{
$return = [];
// repetition data:
/** @var null|array $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.
*/
private function getTransactionData(): array
{
$return = [];
// transaction data:
/** @var null|array $transactions */
$transactions = $this->get('transactions');
if (null === $transactions) {
return [];
}
/** @var array $transaction */
foreach ($transactions as $transaction) {
$return[] = $this->getSingleTransactionData($transaction);
}
return $return;
}
/**
* The rules that the incoming request must be matched against.
*/
@@ -207,4 +143,68 @@ class UpdateRequest extends FormRequest
Log::channel('audit')->error(sprintf('Validation errors in %s', self::class), $validator->errors()->toArray());
}
}
/**
* Returns the repetition data as it is found in the submitted data.
*/
private function getRepetitionData(): ?array
{
$return = [];
// repetition data:
/** @var null|array $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.
*/
private function getTransactionData(): array
{
$return = [];
// transaction data:
/** @var null|array $transactions */
$transactions = $this->get('transactions');
if (null === $transactions) {
return [];
}
/** @var array $transaction */
foreach ($transactions as $transaction) {
$return[] = $this->getSingleTransactionData($transaction);
}
return $return;
}
}

View File

@@ -65,43 +65,6 @@ class StoreRequest extends FormRequest
return $data;
}
private function getRuleTriggers(): array
{
$triggers = $this->get('triggers');
$return = [];
if (is_array($triggers)) {
foreach ($triggers as $trigger) {
$return[] = [
'type' => $trigger['type'] ?? '',
'value' => $trigger['value'] ?? null,
'prohibited' => $this->convertBoolean((string) ($trigger['prohibited'] ?? 'false')),
'active' => $this->convertBoolean((string) ($trigger['active'] ?? 'true')),
'stop_processing' => $this->convertBoolean((string) ($trigger['stop_processing'] ?? 'false')),
];
}
}
return $return;
}
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.
*/
@@ -150,19 +113,6 @@ class StoreRequest extends FormRequest
}
}
/**
* Adds an error to the validator when there are no triggers in the array of data.
*/
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.
*/
@@ -176,6 +126,35 @@ class StoreRequest extends FormRequest
}
}
/**
* Adds an error to the validator when there are no ACTIVE actions in the array of data.
*/
protected function atLeastOneActiveAction(Validator $validator): void
{
$data = $validator->getData();
/** @var null|array|int|string $actions */
$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 ($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.
*/
@@ -206,31 +185,52 @@ class StoreRequest extends FormRequest
}
/**
* Adds an error to the validator when there are no ACTIVE actions in the array of data.
* Adds an error to the validator when there are no triggers in the array of data.
*/
protected function atLeastOneActiveAction(Validator $validator): void
protected function atLeastOneTrigger(Validator $validator): void
{
$data = $validator->getData();
/** @var null|array|int|string $actions */
$actions = $data['actions'] ?? [];
$data = $validator->getData();
$triggers = $data['triggers'] ?? [];
// 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 ($allInactive) {
$validator->errors()->add(sprintf('actions.%d.active', $inactiveIndex), (string) trans('validation.at_least_one_active_action'));
if (!is_countable($triggers) || 0 === count($triggers)) {
$validator->errors()->add('title', (string) trans('validation.at_least_one_trigger'));
}
}
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;
}
private function getRuleTriggers(): array
{
$triggers = $this->get('triggers');
$return = [];
if (is_array($triggers)) {
foreach ($triggers as $trigger) {
$return[] = [
'type' => $trigger['type'] ?? '',
'value' => $trigger['value'] ?? null,
'prohibited' => $this->convertBoolean((string) ($trigger['prohibited'] ?? 'false')),
'active' => $this->convertBoolean((string) ($trigger['active'] ?? 'true')),
'stop_processing' => $this->convertBoolean((string) ($trigger['stop_processing'] ?? 'false')),
];
}
}
return $return;
}
}

View File

@@ -42,9 +42,19 @@ class TestRequest extends FormRequest
return ['page' => $this->getPage(), 'start' => $this->getDate('start'), 'end' => $this->getDate('end'), 'accounts' => $this->getAccounts()];
}
private function getPage(): int
public function rules(): array
{
return 0 === (int) $this->query('page') ? 1 : (int) $this->query('page');
return [
'start' => 'date|after:1970-01-02|before:2038-01-17',
'end' => 'date|after_or_equal:start|after:1970-01-02|before:2038-01-17',
'accounts' => '',
'accounts.*' => 'required|exists:accounts,id|belongsToUser:accounts',
];
}
private function getAccounts(): array
{
return $this->get('accounts') ?? [];
}
private function getDate(string $field): ?Carbon
@@ -58,18 +68,8 @@ class TestRequest extends FormRequest
return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10));
}
private function getAccounts(): array
private function getPage(): int
{
return $this->get('accounts') ?? [];
}
public function rules(): array
{
return [
'start' => 'date|after:1970-01-02|before:2038-01-17',
'end' => 'date|after_or_equal:start|after:1970-01-02|before:2038-01-17',
'accounts' => '',
'accounts.*' => 'required|exists:accounts,id|belongsToUser:accounts',
];
return 0 === (int) $this->query('page') ? 1 : (int) $this->query('page');
}
}

View File

@@ -42,6 +42,21 @@ class TriggerRequest extends FormRequest
return ['start' => $this->getDate('start'), 'end' => $this->getDate('end'), 'accounts' => $this->getAccounts()];
}
public function rules(): array
{
return [
'start' => 'date|after:1970-01-02|before:2038-01-17',
'end' => 'date|after_or_equal:start|after:1970-01-02|before:2038-01-17',
'accounts' => '',
'accounts.*' => 'exists:accounts,id|belongsToUser:accounts',
];
}
private function getAccounts(): array
{
return $this->get('accounts') ?? [];
}
private function getDate(string $field): ?Carbon
{
$value = $this->query($field);
@@ -52,19 +67,4 @@ class TriggerRequest extends FormRequest
return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10));
}
private function getAccounts(): array
{
return $this->get('accounts') ?? [];
}
public function rules(): array
{
return [
'start' => 'date|after:1970-01-02|before:2038-01-17',
'end' => 'date|after_or_equal:start|after:1970-01-02|before:2038-01-17',
'accounts' => '',
'accounts.*' => 'exists:accounts,id|belongsToUser:accounts',
];
}
}

View File

@@ -72,52 +72,6 @@ class UpdateRequest extends FormRequest
return $return;
}
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;
$prohibited = array_key_exists('prohibited', $trigger) ? $trigger['prohibited'] : false;
$stopProcessing = array_key_exists('stop_processing', $trigger) ? $trigger['stop_processing'] : false;
$return[] = [
'type' => $trigger['type'],
'value' => $trigger['value'],
'prohibited' => $prohibited,
'active' => $active,
'stop_processing' => $stopProcessing,
];
}
}
return $return;
}
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.
*/
@@ -170,46 +124,6 @@ class UpdateRequest extends FormRequest
}
}
/**
* Adds an error to the validator when there are no repetitions in the array of data.
*/
protected function atLeastOneTrigger(Validator $validator): void
{
$data = $validator->getData();
$triggers = $data['triggers'] ?? null;
// need at least one trigger
if (is_array($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.
*/
protected function atLeastOneValidTrigger(Validator $validator): void
{
$data = $validator->getData();
$triggers = $data['triggers'] ?? [];
$allInactive = true;
$inactiveIndex = 0;
// need at least one trigger
if (is_array($triggers) && 0 === count($triggers)) {
return;
}
foreach ($triggers as $index => $trigger) {
$active = array_key_exists('active', $trigger) ? $trigger['active'] : true; // assume true
if (true === $active) {
$allInactive = false;
}
if (false === $active) {
$inactiveIndex = $index;
}
}
if ($allInactive) {
$validator->errors()->add(sprintf('triggers.%d.active', $inactiveIndex), (string) trans('validation.at_least_one_active_trigger'));
}
}
/**
* Adds an error to the validator when there are no repetitions in the array of data.
*/
@@ -223,6 +137,19 @@ class UpdateRequest extends FormRequest
}
}
/**
* Adds an error to the validator when there are no repetitions in the array of data.
*/
protected function atLeastOneTrigger(Validator $validator): void
{
$data = $validator->getData();
$triggers = $data['triggers'] ?? null;
// need at least one trigger
if (is_array($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.
*/
@@ -250,4 +177,77 @@ class UpdateRequest extends FormRequest
$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.
*/
protected function atLeastOneValidTrigger(Validator $validator): void
{
$data = $validator->getData();
$triggers = $data['triggers'] ?? [];
$allInactive = true;
$inactiveIndex = 0;
// need at least one trigger
if (is_array($triggers) && 0 === count($triggers)) {
return;
}
foreach ($triggers as $index => $trigger) {
$active = array_key_exists('active', $trigger) ? $trigger['active'] : true; // assume true
if (true === $active) {
$allInactive = false;
}
if (false === $active) {
$inactiveIndex = $index;
}
}
if ($allInactive) {
$validator->errors()->add(sprintf('triggers.%d.active', $inactiveIndex), (string) trans('validation.at_least_one_active_trigger'));
}
}
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;
}
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;
$prohibited = array_key_exists('prohibited', $trigger) ? $trigger['prohibited'] : false;
$stopProcessing = array_key_exists('stop_processing', $trigger) ? $trigger['stop_processing'] : false;
$return[] = [
'type' => $trigger['type'],
'value' => $trigger['value'],
'prohibited' => $prohibited,
'active' => $active,
'stop_processing' => $stopProcessing,
];
}
}
return $return;
}
}

View File

@@ -42,6 +42,21 @@ class TestRequest extends FormRequest
return ['start' => $this->getDate('start'), 'end' => $this->getDate('end'), 'accounts' => $this->getAccounts()];
}
public function rules(): array
{
return [
'start' => 'date|after:1970-01-02|before:2038-01-17',
'end' => 'date|after_or_equal:start|after:1970-01-02|before:2038-01-17',
'accounts' => '',
'accounts.*' => 'exists:accounts,id|belongsToUser:accounts',
];
}
private function getAccounts(): array
{
return $this->get('accounts') ?? [];
}
private function getDate(string $field): ?Carbon
{
$value = $this->query($field);
@@ -52,19 +67,4 @@ class TestRequest extends FormRequest
return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10));
}
private function getAccounts(): array
{
return $this->get('accounts') ?? [];
}
public function rules(): array
{
return [
'start' => 'date|after:1970-01-02|before:2038-01-17',
'end' => 'date|after_or_equal:start|after:1970-01-02|before:2038-01-17',
'accounts' => '',
'accounts.*' => 'exists:accounts,id|belongsToUser:accounts',
];
}
}

View File

@@ -42,15 +42,9 @@ class TriggerRequest extends FormRequest
return ['start' => $this->getDate('start'), 'end' => $this->getDate('end'), 'accounts' => $this->getAccounts()];
}
private function getDate(string $field): ?Carbon
public function rules(): array
{
$value = $this->query($field);
if (is_array($value)) {
return null;
}
$value = (string) $value;
return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10));
return ['start' => 'date|after:1970-01-02|before:2038-01-17', 'end' => 'date|after_or_equal:start|after:1970-01-02|before:2038-01-17'];
}
private function getAccounts(): array
@@ -62,8 +56,14 @@ class TriggerRequest extends FormRequest
return $this->get('accounts');
}
public function rules(): array
private function getDate(string $field): ?Carbon
{
return ['start' => 'date|after:1970-01-02|before:2038-01-17', 'end' => 'date|after_or_equal:start|after:1970-01-02|before:2038-01-17'];
$value = $this->query($field);
if (is_array($value)) {
return null;
}
$value = (string) $value;
return null === $this->query($field) ? null : Carbon::createFromFormat('Y-m-d', substr($value, 0, 10));
}
}

View File

@@ -73,106 +73,6 @@ class StoreRequest extends FormRequest
// TODO include location and ability to process it.
}
/**
* Get transaction data.
*/
private function getTransactionData(): array
{
$return = [];
/**
* @var array $transaction
*/
foreach ($this->get('transactions') as $transaction) {
$object = new NullArrayObject($transaction);
$return[] = [
'type' => $this->clearString($object['type']),
'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']),
// location
'latitude' => $this->floatFromValue((string) $object['latitude']),
'longitude' => $this->floatFromValue((string) $object['longitude']),
'zoom_level' => $this->integerFromValue((string) $object['zoom_level']),
// foreign currency info:
'foreign_currency_id' => $this->integerFromValue((string) $object['foreign_currency_id']),
'foreign_currency_code' => $this->clearString((string) $object['foreign_currency_code']),
// amount and foreign amount. Cannot be 0.
'amount' => $this->clearString((string) $object['amount']),
'foreign_amount' => $this->clearString((string) $object['foreign_amount']),
// description.
'description' => $this->clearString($object['description']),
// 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']),
'source_iban' => $this->clearIban((string) $object['source_iban']),
'source_number' => $this->clearString((string) $object['source_number']),
'source_bic' => $this->clearString((string) $object['source_bic']),
// 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']),
'destination_iban' => $this->clearIban((string) $object['destination_iban']),
'destination_number' => $this->clearString((string) $object['destination_number']),
'destination_bic' => $this->clearString((string) $object['destination_bic']),
// budget info
'budget_id' => $this->integerFromValue((string) $object['budget_id']),
'budget_name' => $this->clearString((string) $object['budget_name']),
// category info
'category_id' => $this->integerFromValue((string) $object['category_id']),
'category_name' => $this->clearString((string) $object['category_name']),
// 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']),
// 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']),
// some other interesting properties
'reconciled' => $this->convertBoolean((string) $object['reconciled']),
'notes' => $this->clearStringKeepNewlines((string) $object['notes']),
'tags' => $this->arrayFromValue($object['tags']),
// all custom fields:
'internal_reference' => $this->clearString((string) $object['internal_reference']),
'external_id' => $this->clearString((string) $object['external_id']),
'original_source' => sprintf('ff3-v%s', config('firefly.version')),
'recurrence_id' => $this->integerFromValue($object['recurrence_id']),
'bunq_payment_id' => $this->clearString((string) $object['bunq_payment_id']),
'external_url' => $this->clearString((string) $object['external_url']),
'sepa_cc' => $this->clearString((string) $object['sepa_cc']),
'sepa_ct_op' => $this->clearString((string) $object['sepa_ct_op']),
'sepa_ct_id' => $this->clearString((string) $object['sepa_ct_id']),
'sepa_db' => $this->clearString((string) $object['sepa_db']),
'sepa_country' => $this->clearString((string) $object['sepa_country']),
'sepa_ep' => $this->clearString((string) $object['sepa_ep']),
'sepa_ci' => $this->clearString((string) $object['sepa_ci']),
'sepa_batch_id' => $this->clearString((string) $object['sepa_batch_id']),
// 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.
*/
@@ -305,4 +205,104 @@ class StoreRequest extends FormRequest
Log::channel('audit')->error(sprintf('Validation errors in %s', self::class), $validator->errors()->toArray());
}
}
/**
* Get transaction data.
*/
private function getTransactionData(): array
{
$return = [];
/**
* @var array $transaction
*/
foreach ($this->get('transactions') as $transaction) {
$object = new NullArrayObject($transaction);
$return[] = [
'type' => $this->clearString($object['type']),
'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']),
// location
'latitude' => $this->floatFromValue((string) $object['latitude']),
'longitude' => $this->floatFromValue((string) $object['longitude']),
'zoom_level' => $this->integerFromValue((string) $object['zoom_level']),
// foreign currency info:
'foreign_currency_id' => $this->integerFromValue((string) $object['foreign_currency_id']),
'foreign_currency_code' => $this->clearString((string) $object['foreign_currency_code']),
// amount and foreign amount. Cannot be 0.
'amount' => $this->clearString((string) $object['amount']),
'foreign_amount' => $this->clearString((string) $object['foreign_amount']),
// description.
'description' => $this->clearString($object['description']),
// 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']),
'source_iban' => $this->clearIban((string) $object['source_iban']),
'source_number' => $this->clearString((string) $object['source_number']),
'source_bic' => $this->clearString((string) $object['source_bic']),
// 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']),
'destination_iban' => $this->clearIban((string) $object['destination_iban']),
'destination_number' => $this->clearString((string) $object['destination_number']),
'destination_bic' => $this->clearString((string) $object['destination_bic']),
// budget info
'budget_id' => $this->integerFromValue((string) $object['budget_id']),
'budget_name' => $this->clearString((string) $object['budget_name']),
// category info
'category_id' => $this->integerFromValue((string) $object['category_id']),
'category_name' => $this->clearString((string) $object['category_name']),
// 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']),
// 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']),
// some other interesting properties
'reconciled' => $this->convertBoolean((string) $object['reconciled']),
'notes' => $this->clearStringKeepNewlines((string) $object['notes']),
'tags' => $this->arrayFromValue($object['tags']),
// all custom fields:
'internal_reference' => $this->clearString((string) $object['internal_reference']),
'external_id' => $this->clearString((string) $object['external_id']),
'original_source' => sprintf('ff3-v%s', config('firefly.version')),
'recurrence_id' => $this->integerFromValue($object['recurrence_id']),
'bunq_payment_id' => $this->clearString((string) $object['bunq_payment_id']),
'external_url' => $this->clearString((string) $object['external_url']),
'sepa_cc' => $this->clearString((string) $object['sepa_cc']),
'sepa_ct_op' => $this->clearString((string) $object['sepa_ct_op']),
'sepa_ct_id' => $this->clearString((string) $object['sepa_ct_id']),
'sepa_db' => $this->clearString((string) $object['sepa_db']),
'sepa_country' => $this->clearString((string) $object['sepa_country']),
'sepa_ep' => $this->clearString((string) $object['sepa_ep']),
'sepa_ci' => $this->clearString((string) $object['sepa_ci']),
'sepa_batch_id' => $this->clearString((string) $object['sepa_batch_id']),
// 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

@@ -133,158 +133,6 @@ class UpdateRequest extends FormRequest
return $data;
}
/**
* Get transaction data.
*
* @throws FireflyException
*/
private function getTransactionData(): array
{
Log::debug(sprintf('Now in %s', __METHOD__));
$return = [];
/** @var null|array $transactions */
$transactions = $this->get('transactions');
if (!is_countable($transactions)) {
return $return;
}
/** @var null|array $transaction */
foreach ($transactions as $transaction) {
if (!is_array($transaction)) {
throw new FireflyException('Invalid data submitted: transaction is not array.');
}
// default response is to update nothing in the transaction:
$current = [];
$current = $this->getIntegerData($current, $transaction);
$current = $this->getStringData($current, $transaction);
$current = $this->getNlStringData($current, $transaction);
$current = $this->getDateData($current, $transaction);
$current = $this->getBooleanData($current, $transaction);
$current = $this->getArrayData($current, $transaction);
$current = $this->getFloatData($current, $transaction);
$return[] = $current;
}
return $return;
}
/**
* For each field, add it to the array if a reference is present in the request:
*
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getIntegerData(array $current, array $transaction): array
{
foreach ($this->integerFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->integerFromValue((string) $transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getStringData(array $current, array $transaction): array
{
foreach ($this->stringFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->clearString((string) $transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getNlStringData(array $current, array $transaction): array
{
foreach ($this->textareaFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->clearStringKeepNewlines((string) $transaction[$fieldName]); // keep newlines
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getDateData(array $current, array $transaction): array
{
foreach ($this->dateFields as $fieldName) {
Log::debug(sprintf('Now at date field %s', $fieldName));
if (array_key_exists($fieldName, $transaction)) {
Log::debug(sprintf('New value: "%s"', $transaction[$fieldName]));
$current[$fieldName] = $this->dateFromValue((string) $transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getBooleanData(array $current, array $transaction): array
{
foreach ($this->booleanFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->convertBoolean((string) $transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getArrayData(array $current, array $transaction): array
{
foreach ($this->arrayFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->arrayFromValue($transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getFloatData(array $current, array $transaction): array
{
foreach ($this->floatFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$value = $transaction[$fieldName];
if (is_float($value)) {
$current[$fieldName] = sprintf('%.12f', $value);
}
if (!is_float($value)) {
$current[$fieldName] = (string) $value;
}
}
}
return $current;
}
/**
* The rules that the incoming request must be matched against.
*/
@@ -406,4 +254,156 @@ class UpdateRequest extends FormRequest
Log::channel('audit')->error(sprintf('Validation errors in %s', self::class), $validator->errors()->toArray());
}
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getArrayData(array $current, array $transaction): array
{
foreach ($this->arrayFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->arrayFromValue($transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getBooleanData(array $current, array $transaction): array
{
foreach ($this->booleanFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->convertBoolean((string) $transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getDateData(array $current, array $transaction): array
{
foreach ($this->dateFields as $fieldName) {
Log::debug(sprintf('Now at date field %s', $fieldName));
if (array_key_exists($fieldName, $transaction)) {
Log::debug(sprintf('New value: "%s"', $transaction[$fieldName]));
$current[$fieldName] = $this->dateFromValue((string) $transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getFloatData(array $current, array $transaction): array
{
foreach ($this->floatFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$value = $transaction[$fieldName];
if (is_float($value)) {
$current[$fieldName] = sprintf('%.12f', $value);
}
if (!is_float($value)) {
$current[$fieldName] = (string) $value;
}
}
}
return $current;
}
/**
* For each field, add it to the array if a reference is present in the request:
*
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getIntegerData(array $current, array $transaction): array
{
foreach ($this->integerFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->integerFromValue((string) $transaction[$fieldName]);
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getNlStringData(array $current, array $transaction): array
{
foreach ($this->textareaFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->clearStringKeepNewlines((string) $transaction[$fieldName]); // keep newlines
}
}
return $current;
}
/**
* @param array<string, string> $current
* @param array<string, mixed> $transaction
*/
private function getStringData(array $current, array $transaction): array
{
foreach ($this->stringFields as $fieldName) {
if (array_key_exists($fieldName, $transaction)) {
$current[$fieldName] = $this->clearString((string) $transaction[$fieldName]);
}
}
return $current;
}
/**
* Get transaction data.
*
* @throws FireflyException
*/
private function getTransactionData(): array
{
Log::debug(sprintf('Now in %s', __METHOD__));
$return = [];
/** @var null|array $transactions */
$transactions = $this->get('transactions');
if (!is_countable($transactions)) {
return $return;
}
/** @var null|array $transaction */
foreach ($transactions as $transaction) {
if (!is_array($transaction)) {
throw new FireflyException('Invalid data submitted: transaction is not array.');
}
// default response is to update nothing in the transaction:
$current = [];
$current = $this->getIntegerData($current, $transaction);
$current = $this->getStringData($current, $transaction);
$current = $this->getNlStringData($current, $transaction);
$current = $this->getDateData($current, $transaction);
$current = $this->getBooleanData($current, $transaction);
$current = $this->getArrayData($current, $transaction);
$current = $this->getFloatData($current, $transaction);
$return[] = $current;
}
return $return;
}
}

View File

@@ -62,14 +62,6 @@ class ConvertsDatesToUTC extends Command
return Command::SUCCESS;
}
private function ConvertModeltoUTC(string $model, array $fields): void
{
/** @var string $field */
foreach ($fields as $field) {
$this->convertFieldtoUTC($model, $field);
}
}
private function convertFieldtoUTC(string $model, string $field): void
{
$this->info(sprintf('Converting %s.%s to UTC', $model, $field));
@@ -98,4 +90,12 @@ class ConvertsDatesToUTC extends Command
$item->save();
});
}
private function ConvertModeltoUTC(string $model, array $fields): void
{
/** @var string $field */
foreach ($fields as $field) {
$this->convertFieldtoUTC($model, $field);
}
}
}

View File

@@ -123,56 +123,17 @@ class CorrectsAccountTypes extends Command
return 0;
}
private function stupidLaravel(): void
private function canCreateDestination(array $validDestinations): bool
{
$this->count = 0;
return in_array(AccountTypeEnum::EXPENSE->value, $validDestinations, true);
}
private function inspectJournal(TransactionJournal $journal): void
/**
* Can only create revenue accounts out of the blue.
*/
private function canCreateSource(array $validSources): bool
{
Log::debug(sprintf('Now inspecting journal #%d', $journal->id));
$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('[a] 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('[b] Going to fix journal #%d', $journal->id));
$this->fixJournal($journal, $type, $sourceTransaction, $destTransaction);
}
}
private function getSourceTransaction(TransactionJournal $journal): Transaction
{
return $journal->transactions->firstWhere('amount', '<', 0);
}
private function getDestinationTransaction(TransactionJournal $journal): Transaction
{
return $journal->transactions->firstWhere('amount', '>', 0);
return in_array(AccountTypeEnum::REVENUE->value, $validSources, true);
}
private function fixJournal(TransactionJournal $journal, string $transactionType, Transaction $source, Transaction $dest): void
@@ -266,12 +227,113 @@ class CorrectsAccountTypes extends Command
}
}
private function shouldBeTransfer(string $transactionType, string $sourceType, string $destinationType): bool
private function getDestinationTransaction(TransactionJournal $journal): Transaction
{
return
TransactionTypeEnum::TRANSFER->value === $transactionType
&& AccountTypeEnum::ASSET->value === $sourceType
&& $this->isLiability($destinationType);
return $journal->transactions->firstWhere('amount', '>', 0);
}
private function getSourceTransaction(TransactionJournal $journal): Transaction
{
return $journal->transactions->firstWhere('amount', '<', 0);
}
private function giveNewDestinationAccount(TransactionJournal $journal, Account $newDestination): void
{
$destTransaction = $this->getDestinationTransaction($journal);
$oldDest = $destTransaction->account;
$destTransaction->account_id = $newDestination->id;
$destTransaction->save();
$message = sprintf(
'Transaction journal #%d, destination account changed from #%d ("%s") to #%d ("%s").',
$journal->id,
$oldDest->id,
$oldDest->name,
$newDestination->id,
$newDestination->name
);
$this->friendlyInfo($message);
$journal->refresh();
Log::debug($message);
}
private function giveNewExpense(TransactionJournal $journal, Transaction $destination): void
{
Log::debug(sprintf('An account of type "%s" could be a valid destination.', AccountTypeEnum::EXPENSE->value));
$this->factory->setUser($journal->user);
$name = $destination->account->name;
$newDestination = $this->factory->findOrCreate($name, AccountTypeEnum::EXPENSE->value);
$destination->account()->associate($newDestination);
$destination->save();
$this->friendlyPositive(sprintf(
'Firefly III gave transaction #%d a new destination %s: #%d ("%s").',
$journal->transaction_group_id,
AccountTypeEnum::EXPENSE->value,
$newDestination->id,
$newDestination->name
));
Log::debug(sprintf('Associated account #%d with transaction #%d', $newDestination->id, $destination->id));
$this->inspectJournal($journal);
}
private function giveNewRevenue(TransactionJournal $journal, Transaction $source): void
{
Log::debug(sprintf('An account of type "%s" could be a valid source.', AccountTypeEnum::REVENUE->value));
$this->factory->setUser($journal->user);
$name = $source->account->name;
$newSource = $this->factory->findOrCreate($name, AccountTypeEnum::REVENUE->value);
$source->account()->associate($newSource);
$source->save();
$this->friendlyPositive(sprintf(
'Firefly III gave transaction #%d a new source %s: #%d ("%s").',
$journal->transaction_group_id,
AccountTypeEnum::REVENUE->value,
$newSource->id,
$newSource->name
));
Log::debug(sprintf('Associated account #%d with transaction #%d', $newSource->id, $source->id));
$this->inspectJournal($journal);
}
private function hasValidAccountType(array $validTypes, string $accountType): bool
{
return in_array($accountType, $validTypes, true);
}
private function inspectJournal(TransactionJournal $journal): void
{
Log::debug(sprintf('Now inspecting journal #%d', $journal->id));
$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('[a] 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('[b] Going to fix journal #%d', $journal->id));
$this->fixJournal($journal, $type, $sourceTransaction, $destTransaction);
}
}
private function isLiability(string $destinationType): bool
@@ -282,27 +344,6 @@ class CorrectsAccountTypes extends Command
|| AccountTypeEnum::MORTGAGE->value === $destinationType;
}
private function makeTransfer(TransactionJournal $journal): void
{
// from an asset to a liability should be a withdrawal:
$withdrawal = TransactionType::whereType(TransactionTypeEnum::WITHDRAWAL->value)->first();
$journal->transactionType()->associate($withdrawal);
$journal->save();
$message = sprintf('Converted transaction #%d from a transfer to a withdrawal.', $journal->id);
$this->friendlyInfo($message);
Log::debug($message);
// check it again:
$this->inspectJournal($journal);
}
private function shouldBeDeposit(string $transactionType, string $sourceType, string $destinationType): bool
{
return
TransactionTypeEnum::TRANSFER->value === $transactionType
&& $this->isLiability($sourceType)
&& AccountTypeEnum::ASSET->value === $destinationType;
}
private function makeDeposit(TransactionJournal $journal): void
{
// from a liability to an asset should be a deposit.
@@ -316,14 +357,6 @@ class CorrectsAccountTypes extends Command
$this->inspectJournal($journal);
}
private function shouldGoToExpenseAccount(string $transactionType, string $sourceType, string $destinationType): bool
{
return
TransactionTypeEnum::WITHDRAWAL->value === $transactionType
&& AccountTypeEnum::ASSET->value === $sourceType
&& AccountTypeEnum::REVENUE->value === $destinationType;
}
private function makeExpenseDestination(TransactionJournal $journal, Transaction $destination): void
{
// withdrawals with a revenue account as destination instead of an expense account.
@@ -345,14 +378,6 @@ class CorrectsAccountTypes extends Command
$this->inspectJournal($journal);
}
private function shouldComeFromRevenueAccount(string $transactionType, string $sourceType, string $destinationType): bool
{
return
TransactionTypeEnum::DEPOSIT->value === $transactionType
&& AccountTypeEnum::EXPENSE->value === $sourceType
&& AccountTypeEnum::ASSET->value === $destinationType;
}
private function makeRevenueSource(TransactionJournal $journal, Transaction $source): void
{
// deposits with an expense account as source instead of a revenue account.
@@ -375,78 +400,53 @@ class CorrectsAccountTypes extends Command
$this->inspectJournal($journal);
}
/**
* Can only create revenue accounts out of the blue.
*/
private function canCreateSource(array $validSources): bool
private function makeTransfer(TransactionJournal $journal): void
{
return in_array(AccountTypeEnum::REVENUE->value, $validSources, true);
}
private function hasValidAccountType(array $validTypes, string $accountType): bool
{
return in_array($accountType, $validTypes, true);
}
private function giveNewRevenue(TransactionJournal $journal, Transaction $source): void
{
Log::debug(sprintf('An account of type "%s" could be a valid source.', AccountTypeEnum::REVENUE->value));
$this->factory->setUser($journal->user);
$name = $source->account->name;
$newSource = $this->factory->findOrCreate($name, AccountTypeEnum::REVENUE->value);
$source->account()->associate($newSource);
$source->save();
$this->friendlyPositive(sprintf(
'Firefly III gave transaction #%d a new source %s: #%d ("%s").',
$journal->transaction_group_id,
AccountTypeEnum::REVENUE->value,
$newSource->id,
$newSource->name
));
Log::debug(sprintf('Associated account #%d with transaction #%d', $newSource->id, $source->id));
$this->inspectJournal($journal);
}
private function canCreateDestination(array $validDestinations): bool
{
return in_array(AccountTypeEnum::EXPENSE->value, $validDestinations, true);
}
private function giveNewExpense(TransactionJournal $journal, Transaction $destination): void
{
Log::debug(sprintf('An account of type "%s" could be a valid destination.', AccountTypeEnum::EXPENSE->value));
$this->factory->setUser($journal->user);
$name = $destination->account->name;
$newDestination = $this->factory->findOrCreate($name, AccountTypeEnum::EXPENSE->value);
$destination->account()->associate($newDestination);
$destination->save();
$this->friendlyPositive(sprintf(
'Firefly III gave transaction #%d a new destination %s: #%d ("%s").',
$journal->transaction_group_id,
AccountTypeEnum::EXPENSE->value,
$newDestination->id,
$newDestination->name
));
Log::debug(sprintf('Associated account #%d with transaction #%d', $newDestination->id, $destination->id));
$this->inspectJournal($journal);
}
private function giveNewDestinationAccount(TransactionJournal $journal, Account $newDestination): void
{
$destTransaction = $this->getDestinationTransaction($journal);
$oldDest = $destTransaction->account;
$destTransaction->account_id = $newDestination->id;
$destTransaction->save();
$message = sprintf(
'Transaction journal #%d, destination account changed from #%d ("%s") to #%d ("%s").',
$journal->id,
$oldDest->id,
$oldDest->name,
$newDestination->id,
$newDestination->name
);
// from an asset to a liability should be a withdrawal:
$withdrawal = TransactionType::whereType(TransactionTypeEnum::WITHDRAWAL->value)->first();
$journal->transactionType()->associate($withdrawal);
$journal->save();
$message = sprintf('Converted transaction #%d from a transfer to a withdrawal.', $journal->id);
$this->friendlyInfo($message);
$journal->refresh();
Log::debug($message);
// check it again:
$this->inspectJournal($journal);
}
private function shouldBeDeposit(string $transactionType, string $sourceType, string $destinationType): bool
{
return
TransactionTypeEnum::TRANSFER->value === $transactionType
&& $this->isLiability($sourceType)
&& AccountTypeEnum::ASSET->value === $destinationType;
}
private function shouldBeTransfer(string $transactionType, string $sourceType, string $destinationType): bool
{
return
TransactionTypeEnum::TRANSFER->value === $transactionType
&& AccountTypeEnum::ASSET->value === $sourceType
&& $this->isLiability($destinationType);
}
private function shouldComeFromRevenueAccount(string $transactionType, string $sourceType, string $destinationType): bool
{
return
TransactionTypeEnum::DEPOSIT->value === $transactionType
&& AccountTypeEnum::EXPENSE->value === $sourceType
&& AccountTypeEnum::ASSET->value === $destinationType;
}
private function shouldGoToExpenseAccount(string $transactionType, string $sourceType, string $destinationType): bool
{
return
TransactionTypeEnum::WITHDRAWAL->value === $transactionType
&& AccountTypeEnum::ASSET->value === $sourceType
&& AccountTypeEnum::REVENUE->value === $destinationType;
}
private function stupidLaravel(): void
{
$this->count = 0;
}
}

View File

@@ -253,27 +253,6 @@ class CorrectsAmounts extends Command
$this->friendlyInfo(sprintf('Corrected %d recurring transaction amount(s).', $count));
}
/**
* Foreach loop is unavoidable here.
*/
private function fixRuleTriggers(): void
{
$set = RuleTrigger::whereIn('trigger_type', ['amount_less', 'amount_more', 'amount_is'])->get();
$fixed = 0;
/** @var RuleTrigger $item */
foreach ($set as $item) {
$result = $this->fixRuleTrigger($item);
if ($result) {
++$fixed;
}
}
if (0 === $fixed) {
return;
}
$this->friendlyInfo(sprintf('Corrected %d rule trigger amount(s).', $fixed));
}
private function fixRuleTrigger(RuleTrigger $item): bool
{
try {
@@ -301,6 +280,27 @@ class CorrectsAmounts extends Command
return false;
}
/**
* Foreach loop is unavoidable here.
*/
private function fixRuleTriggers(): void
{
$set = RuleTrigger::whereIn('trigger_type', ['amount_less', 'amount_more', 'amount_is'])->get();
$fixed = 0;
/** @var RuleTrigger $item */
foreach ($set as $item) {
$result = $this->fixRuleTrigger($item);
if ($result) {
++$fixed;
}
}
if (0 === $fixed) {
return;
}
$this->friendlyInfo(sprintf('Corrected %d rule trigger amount(s).', $fixed));
}
private function validateJournal(TransactionJournal $journal): bool
{
$countSource = $journal->transactions()->where('amount', '<', 0)->count();

View File

@@ -26,7 +26,9 @@ namespace FireflyIII\Console\Commands\Correction;
use FireflyIII\Console\Commands\ShowsFriendlyMessages;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventFlags;
use FireflyIII\Events\Model\TransactionGroup\TransactionGroupEventObjects;
use FireflyIII\Events\Model\TransactionGroup\UpdatedSingleTransactionGroup;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use Illuminate\Console\Command;
@@ -44,8 +46,8 @@ class CorrectsGroupAccounts extends Command
*/
public function handle(): int
{
$groups = [];
$res = TransactionJournal::groupBy('transaction_group_id')->get(['transaction_group_id', DB::raw('COUNT(transaction_group_id) as the_count')]);
$groups = [];
$res = TransactionJournal::groupBy('transaction_group_id')->get(['transaction_group_id', DB::raw('COUNT(transaction_group_id) as the_count')]);
/** @var TransactionJournal $journal */
foreach ($res as $journal) {
@@ -53,14 +55,17 @@ class CorrectsGroupAccounts extends Command
$groups[] = (int) $journal->transaction_group_id;
}
}
$flags = new TransactionGroupEventFlags();
$flags->applyRules = true;
$flags->fireWebhooks = true;
$flags->recalculateCredit = true;
$objects = new TransactionGroupEventObjects();
foreach ($groups as $groupId) {
$group = TransactionGroup::find($groupId);
$flags = new TransactionGroupEventFlags();
$flags->applyRules = true;
$flags->fireWebhooks = true;
$flags->recalculateCredit = true;
event(new UpdatedSingleTransactionGroup($group, $flags));
$group = TransactionGroup::find($groupId);
$objects->appendFromTransactionGroup($group);
}
event(new UpdatedSingleTransactionGroup($flags, $objects));
event(new WebhookMessagesRequestSending());
return 0;
}

View File

@@ -51,33 +51,6 @@ class CorrectsIbans extends Command
return 0;
}
private function filterIbans(Collection $accounts): void
{
/** @var Account $account */
foreach ($accounts as $account) {
$iban = (string) $account->iban;
$newIban = Steam::filterSpaces($iban);
if ('' !== $iban && $iban !== $newIban) {
$account->iban = $newIban;
$account->save();
$this->friendlyInfo(sprintf('Removed spaces from IBAN of account #%d', $account->id));
++$this->count;
}
// same for account number:
$accountNumber = $account->accountMeta->where('name', 'account_number')->first();
if (null !== $accountNumber) {
$number = (string) $accountNumber->value;
$newNumber = Steam::filterSpaces($number);
if ('' !== $number && $number !== $newNumber) {
$accountNumber->value = $newNumber;
$accountNumber->save();
$this->friendlyInfo(sprintf('Removed spaces from account number of account #%d', $account->id));
++$this->count;
}
}
}
}
private function countAndCorrectIbans(Collection $accounts): void
{
$set = [];
@@ -119,4 +92,31 @@ class CorrectsIbans extends Command
}
}
}
private function filterIbans(Collection $accounts): void
{
/** @var Account $account */
foreach ($accounts as $account) {
$iban = (string) $account->iban;
$newIban = Steam::filterSpaces($iban);
if ('' !== $iban && $iban !== $newIban) {
$account->iban = $newIban;
$account->save();
$this->friendlyInfo(sprintf('Removed spaces from IBAN of account #%d', $account->id));
++$this->count;
}
// same for account number:
$accountNumber = $account->accountMeta->where('name', 'account_number')->first();
if (null !== $accountNumber) {
$number = (string) $accountNumber->value;
$newNumber = Steam::filterSpaces($number);
if ('' !== $number && $number !== $newNumber) {
$accountNumber->value = $newNumber;
$accountNumber->save();
$this->friendlyInfo(sprintf('Removed spaces from account number of account #%d', $account->id));
++$this->count;
}
}
}
}
}

View File

@@ -68,6 +68,11 @@ class CorrectsInvertedBudgetLimits extends Command
$budgetLimit->end_date = $start;
$budgetLimit->saveQuietly();
}
if ($set->count() > 0) {
// FIXME here be a available budget event.
}
if (1 === $set->count()) {
$this->friendlyInfo('Corrected one budget limit to have the right start/end dates.');

View File

@@ -65,16 +65,6 @@ class CorrectsOpeningBalanceCurrencies extends Command
return 0;
}
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', TransactionTypeEnum::OPENING_BALANCE->value)
->get(['transaction_journals.*'])
;
}
private function correctJournal(TransactionJournal $journal): int
{
// get the asset account for this opening balance:
@@ -107,6 +97,25 @@ class CorrectsOpeningBalanceCurrencies extends Command
return null;
}
private function getCurrency(Account $account): TransactionCurrency
{
/** @var AccountRepositoryInterface $repos */
$repos = app(AccountRepositoryInterface::class);
$repos->setUser($account->user);
return $repos->getAccountCurrency($account) ?? Amount::getPrimaryCurrencyByUserGroup($account->userGroup);
}
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', TransactionTypeEnum::OPENING_BALANCE->value)
->get(['transaction_journals.*'])
;
}
private function setCorrectCurrency(Account $account, TransactionJournal $journal): int
{
$currency = $this->getCurrency($account);
@@ -128,13 +137,4 @@ class CorrectsOpeningBalanceCurrencies extends Command
return $count;
}
private function getCurrency(Account $account): TransactionCurrency
{
/** @var AccountRepositoryInterface $repos */
$repos = app(AccountRepositoryInterface::class);
$repos->setUser($account->user);
return $repos->getAccountCurrency($account) ?? Amount::getPrimaryCurrencyByUserGroup($account->userGroup);
}
}

View File

@@ -54,17 +54,6 @@ class CorrectsRecurringTransactions extends Command
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);
}
private function correctTransactions(): void
{
$users = $this->userRepos->all();
@@ -75,17 +64,6 @@ class CorrectsRecurringTransactions extends Command
}
}
private function processUser(User $user): void
{
$this->recurringRepos->setUser($user);
$recurrences = $this->recurringRepos->get();
/** @var Recurrence $recurrence */
foreach ($recurrences as $recurrence) {
$this->processRecurrence($recurrence);
}
}
private function processRecurrence(Recurrence $recurrence): void
{
/** @var RecurrenceTransaction $transaction */
@@ -115,4 +93,26 @@ class CorrectsRecurringTransactions extends Command
}
}
}
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

@@ -67,6 +67,15 @@ class CorrectsTransactionTypes extends Command
return 0;
}
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.
*/
@@ -105,31 +114,6 @@ class CorrectsTransactionTypes extends Command
return false;
}
/**
* @throws FireflyException
*/
private function getSourceAccount(TransactionJournal $journal): Account
{
$collection = $journal->transactions->filter(static fn (Transaction $transaction): bool => $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 null|Account $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;
}
/**
* @throws FireflyException
*/
@@ -155,12 +139,28 @@ class CorrectsTransactionTypes extends Command
return $account;
}
private function changeJournal(TransactionJournal $journal, string $expectedType): void
/**
* @throws FireflyException
*/
private function getSourceAccount(TransactionJournal $journal): Account
{
$type = TransactionType::whereType($expectedType)->first();
if (null !== $type) {
$journal->transaction_type_id = $type->id;
$journal->save();
$collection = $journal->transactions->filter(static fn (Transaction $transaction): bool => $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 null|Account $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

@@ -70,271 +70,6 @@ class CorrectsUnevenAmount extends Command
return 0;
}
private function convertOldStyleTransfers(): void
{
Log::debug('convertOldStyleTransfers()');
// select transactions with a foreign amount and a foreign currency. and it's a transfer. and they are different.
$transactions = Transaction::distinct()
->leftJoin('transaction_journals', 'transaction_journals.id', 'transactions.transaction_journal_id')
->leftJoin('transaction_types', 'transaction_types.id', 'transaction_journals.transaction_type_id')
->where('transaction_types.type', TransactionTypeEnum::TRANSFER->value)
->whereNotNull('foreign_currency_id')
->whereNotNull('foreign_amount')
->get(['transactions.transaction_journal_id'])
;
$count = 0;
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
/** @var null|TransactionJournal $journal */
$journal = TransactionJournal::find($transaction->transaction_journal_id);
if (null === $journal) {
Log::debug('Found no journal, continue.');
continue;
}
// needs to be a transfer.
if (TransactionTypeEnum::TRANSFER->value !== $journal->transactionType->type) {
Log::debug('Must be a transfer, continue.');
continue;
}
/** @var null|Transaction $destination */
$destination = $journal->transactions()->where('amount', '>', 0)->first();
/** @var null|Transaction $source */
$source = $journal->transactions()->where('amount', '<', 0)->first();
if (null === $destination || null === $source) {
Log::debug('Source or destination transaction is NULL, continue.');
// will be picked up later.
continue;
}
if ($source->transaction_currency_id === $destination->transaction_currency_id) {
Log::debug('Ready to swap data between transactions.');
$destination->foreign_currency_id = $source->transaction_currency_id;
$destination->foreign_amount = Steam::positive($source->amount);
$destination->transaction_currency_id = $source->foreign_currency_id;
$destination->amount = Steam::positive($source->foreign_amount);
$destination->balance_dirty = true;
$source->balance_dirty = true;
$destination->save();
$source->save();
$this->friendlyWarning(sprintf('Corrected foreign amounts of transfer #%d.', $journal->id));
++$count;
}
}
if (0 === $count) {
return;
}
$this->friendlyPositive(sprintf('Fixed %d transfer(s) with unbalanced amounts.', $count));
}
private function fixUnevenAmounts(): void
{
Log::debug('fixUnevenAmounts()');
$journals = DB::table('transactions')
->groupBy('transaction_journal_id')
->whereNull('deleted_at')
->get(['transaction_journal_id', DB::raw('SUM(amount) AS the_sum')])
;
/** @var stdClass $entry */
foreach ($journals as $entry) {
$sum = (string) $entry->the_sum;
$sum = Steam::floatalize($sum);
if (
!is_numeric($sum)
|| '' === $sum // @phpstan-ignore-line
|| str_contains($sum, 'e')
|| str_contains($sum, ',')
) {
$message = sprintf('Journal #%d has an invalid sum ("%s"). No sure what to do.', $entry->transaction_journal_id, $entry->the_sum);
$this->friendlyWarning($message);
Log::warning($message);
++$this->count;
continue;
}
$res = -1;
try {
$res = bccomp($sum, '0');
} catch (ValueError $e) {
$this->friendlyError(sprintf('Could not bccomp("%s", "0").', $sum));
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
if (0 !== $res) {
$this->fixJournal((int) $entry->transaction_journal_id);
}
}
}
private function fixJournal(int $param): void
{
// one of the transactions is bad.
$journal = TransactionJournal::find($param);
if (null === $journal) {
return;
}
/** @var null|Transaction $source */
$source = $journal->transactions()->where('amount', '<', 0)->first();
if (null === $source) {
$this->friendlyError(sprintf(
'Journal #%d ("%s") has no source transaction. It will be deleted to maintain database consistency.',
$journal->id ?? 0,
$journal->description ?? ''
));
Transaction::where('transaction_journal_id', $journal->id ?? 0)->forceDelete();
TransactionJournal::where('id', $journal->id ?? 0)->forceDelete();
++$this->count;
return;
}
$amount = bcmul('-1', (string) $source->amount);
// fix amount of destination:
/** @var null|Transaction $destination */
$destination = $journal->transactions()->where('amount', '>', 0)->first();
if (null === $destination) {
$this->friendlyError(sprintf(
'Journal #%d ("%s") has no destination transaction. It will be deleted to maintain database consistency.',
$journal->id ?? 0,
$journal->description ?? ''
));
Transaction::where('transaction_journal_id', $journal->id ?? 0)->forceDelete();
TransactionJournal::where('id', $journal->id ?? 0)->forceDelete();
++$this->count;
return;
}
// may still be able to salvage this journal if it is a transfer with foreign currency info
if ($this->isForeignCurrencyTransfer($journal) || $this->isBetweenAssetAndLiability($journal)) {
Log::debug(sprintf('Can skip foreign currency transfer / asset+liability transaction #%d.', $journal->id));
return;
}
$message = sprintf('Sum of journal #%d is not zero, journal is broken and now fixed.', $journal->id);
$this->friendlyWarning($message);
Log::warning($message);
$destination->amount = $amount;
$destination->save();
$message = sprintf('Corrected amount in transaction journal #%d', $param);
$this->friendlyInfo($message);
++$this->count;
}
private function isForeignCurrencyTransfer(TransactionJournal $journal): bool
{
if (TransactionTypeEnum::TRANSFER->value !== $journal->transactionType->type) {
return false;
}
/** @var Transaction $destination */
$destination = $journal->transactions()->where('amount', '>', 0)->first();
/** @var Transaction $source */
$source = $journal->transactions()->where('amount', '<', 0)->first();
// safety catch on NULL should not be necessary, we just had that catch.
// source amount = dest foreign amount
// source currency = dest foreign currency
// dest amount = source foreign currency
// dest currency = source foreign currency
// Log::debug(sprintf('[a] %s', bccomp(\FireflyIII\Support\Facades\Steam::positive($source->amount), \FireflyIII\Support\Facades\Steam::positive($destination->foreign_amount))));
// Log::debug(sprintf('[b] %s', bccomp(\FireflyIII\Support\Facades\Steam::positive($destination->amount), \FireflyIII\Support\Facades\Steam::positive($source->foreign_amount))));
// Log::debug(sprintf('[c] %s', var_export($source->transaction_currency_id === $destination->foreign_currency_id,true)));
// Log::debug(sprintf('[d] %s', var_export((int) $destination->transaction_currency_id ===(int) $source->foreign_currency_id, true)));
return
0 === bccomp(Steam::positive($source->amount), Steam::positive($destination->foreign_amount))
&& $source->transaction_currency_id === $destination->foreign_currency_id
&& 0 === bccomp(Steam::positive($destination->amount), Steam::positive($source->foreign_amount))
&& (int) $destination->transaction_currency_id === (int) $source->foreign_currency_id;
}
private function matchCurrencies(): void
{
$journals = TransactionJournal::leftJoin('transactions', 'transaction_journals.id', 'transactions.transaction_journal_id')->where(
'transactions.transaction_currency_id',
'!=',
DB::raw('transaction_journals.transaction_currency_id')
)->get(['transaction_journals.*']);
$count = 0;
/** @var TransactionJournal $journal */
foreach ($journals as $journal) {
if (!$this->isForeignCurrencyTransfer($journal) && !$this->isBetweenAssetAndLiability($journal)) {
Transaction::where('transaction_journal_id', $journal->id)->update(['transaction_currency_id' => $journal->transaction_currency_id]);
++$count;
continue;
}
Log::debug(sprintf('Can skip foreign currency transfer or transaction between asset and liability #%d.', $journal->id));
}
if (0 === $count) {
return;
}
$this->friendlyPositive(sprintf('Fixed %d journal(s) with mismatched currencies.', $journals->count()));
}
private function isBetweenAssetAndLiability(TransactionJournal $journal): bool
{
/** @var null|Transaction $sourceTransaction */
$sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first();
/** @var null|Transaction $destinationTransaction */
$destinationTransaction = $journal->transactions()->where('amount', '>', 0)->first();
if (null === $sourceTransaction || null === $destinationTransaction) {
Log::warning('Either transaction is false, stop.');
return false;
}
if (null === $sourceTransaction->foreign_amount || null === $destinationTransaction->foreign_amount) {
Log::warning('Either foreign amount is false, stop.');
return false;
}
$source = $sourceTransaction->account;
$destination = $destinationTransaction->account;
if (null === $source || null === $destination) {
Log::warning('Either is false, stop.');
return false;
}
$sourceTypes = [AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value];
// source is liability, destination is asset
if (in_array($source->accountType->type, $sourceTypes, true) && AccountTypeEnum::ASSET->value === $destination->accountType->type) {
Log::debug('Source is a liability account, destination is an asset account, return TRUE.');
return true;
}
// source is asset, destination is liability
if (in_array($destination->accountType->type, $sourceTypes, true) && AccountTypeEnum::ASSET->value === $source->accountType->type) {
Log::debug('Destination is a liability account, source is an asset account, return TRUE.');
return true;
}
return false;
}
private function convertOldStyleTransactions(): void
{
/** @var AccountRepositoryInterface $repository */
@@ -448,4 +183,269 @@ class CorrectsUnevenAmount extends Command
$this->friendlyPositive(sprintf('Fixed %d journal(s) with unbalanced amounts.', $count));
}
private function convertOldStyleTransfers(): void
{
Log::debug('convertOldStyleTransfers()');
// select transactions with a foreign amount and a foreign currency. and it's a transfer. and they are different.
$transactions = Transaction::distinct()
->leftJoin('transaction_journals', 'transaction_journals.id', 'transactions.transaction_journal_id')
->leftJoin('transaction_types', 'transaction_types.id', 'transaction_journals.transaction_type_id')
->where('transaction_types.type', TransactionTypeEnum::TRANSFER->value)
->whereNotNull('foreign_currency_id')
->whereNotNull('foreign_amount')
->get(['transactions.transaction_journal_id'])
;
$count = 0;
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
/** @var null|TransactionJournal $journal */
$journal = TransactionJournal::find($transaction->transaction_journal_id);
if (null === $journal) {
Log::debug('Found no journal, continue.');
continue;
}
// needs to be a transfer.
if (TransactionTypeEnum::TRANSFER->value !== $journal->transactionType->type) {
Log::debug('Must be a transfer, continue.');
continue;
}
/** @var null|Transaction $destination */
$destination = $journal->transactions()->where('amount', '>', 0)->first();
/** @var null|Transaction $source */
$source = $journal->transactions()->where('amount', '<', 0)->first();
if (null === $destination || null === $source) {
Log::debug('Source or destination transaction is NULL, continue.');
// will be picked up later.
continue;
}
if ($source->transaction_currency_id === $destination->transaction_currency_id) {
Log::debug('Ready to swap data between transactions.');
$destination->foreign_currency_id = $source->transaction_currency_id;
$destination->foreign_amount = Steam::positive($source->amount);
$destination->transaction_currency_id = $source->foreign_currency_id;
$destination->amount = Steam::positive($source->foreign_amount);
$destination->balance_dirty = true;
$source->balance_dirty = true;
$destination->save();
$source->save();
$this->friendlyWarning(sprintf('Corrected foreign amounts of transfer #%d.', $journal->id));
++$count;
}
}
if (0 === $count) {
return;
}
$this->friendlyPositive(sprintf('Fixed %d transfer(s) with unbalanced amounts.', $count));
}
private function fixJournal(int $param): void
{
// one of the transactions is bad.
$journal = TransactionJournal::find($param);
if (null === $journal) {
return;
}
/** @var null|Transaction $source */
$source = $journal->transactions()->where('amount', '<', 0)->first();
if (null === $source) {
$this->friendlyError(sprintf(
'Journal #%d ("%s") has no source transaction. It will be deleted to maintain database consistency.',
$journal->id ?? 0,
$journal->description ?? ''
));
Transaction::where('transaction_journal_id', $journal->id ?? 0)->forceDelete();
TransactionJournal::where('id', $journal->id ?? 0)->forceDelete();
++$this->count;
return;
}
$amount = bcmul('-1', (string) $source->amount);
// fix amount of destination:
/** @var null|Transaction $destination */
$destination = $journal->transactions()->where('amount', '>', 0)->first();
if (null === $destination) {
$this->friendlyError(sprintf(
'Journal #%d ("%s") has no destination transaction. It will be deleted to maintain database consistency.',
$journal->id ?? 0,
$journal->description ?? ''
));
Transaction::where('transaction_journal_id', $journal->id ?? 0)->forceDelete();
TransactionJournal::where('id', $journal->id ?? 0)->forceDelete();
++$this->count;
return;
}
// may still be able to salvage this journal if it is a transfer with foreign currency info
if ($this->isForeignCurrencyTransfer($journal) || $this->isBetweenAssetAndLiability($journal)) {
Log::debug(sprintf('Can skip foreign currency transfer / asset+liability transaction #%d.', $journal->id));
return;
}
$message = sprintf('Sum of journal #%d is not zero, journal is broken and now fixed.', $journal->id);
$this->friendlyWarning($message);
Log::warning($message);
$destination->amount = $amount;
$destination->save();
$message = sprintf('Corrected amount in transaction journal #%d', $param);
$this->friendlyInfo($message);
++$this->count;
}
private function fixUnevenAmounts(): void
{
Log::debug('fixUnevenAmounts()');
$journals = DB::table('transactions')
->groupBy('transaction_journal_id')
->whereNull('deleted_at')
->get(['transaction_journal_id', DB::raw('SUM(amount) AS the_sum')])
;
/** @var stdClass $entry */
foreach ($journals as $entry) {
$sum = (string) $entry->the_sum;
$sum = Steam::floatalize($sum);
if (
!is_numeric($sum)
|| '' === $sum // @phpstan-ignore-line
|| str_contains($sum, 'e')
|| str_contains($sum, ',')
) {
$message = sprintf('Journal #%d has an invalid sum ("%s"). No sure what to do.', $entry->transaction_journal_id, $entry->the_sum);
$this->friendlyWarning($message);
Log::warning($message);
++$this->count;
continue;
}
$res = -1;
try {
$res = bccomp($sum, '0');
} catch (ValueError $e) {
$this->friendlyError(sprintf('Could not bccomp("%s", "0").', $sum));
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
}
if (0 !== $res) {
$this->fixJournal((int) $entry->transaction_journal_id);
}
}
}
private function isBetweenAssetAndLiability(TransactionJournal $journal): bool
{
/** @var null|Transaction $sourceTransaction */
$sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first();
/** @var null|Transaction $destinationTransaction */
$destinationTransaction = $journal->transactions()->where('amount', '>', 0)->first();
if (null === $sourceTransaction || null === $destinationTransaction) {
Log::warning('Either transaction is false, stop.');
return false;
}
if (null === $sourceTransaction->foreign_amount || null === $destinationTransaction->foreign_amount) {
Log::warning('Either foreign amount is false, stop.');
return false;
}
$source = $sourceTransaction->account;
$destination = $destinationTransaction->account;
if (null === $source || null === $destination) {
Log::warning('Either is false, stop.');
return false;
}
$sourceTypes = [AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value];
// source is liability, destination is asset
if (in_array($source->accountType->type, $sourceTypes, true) && AccountTypeEnum::ASSET->value === $destination->accountType->type) {
Log::debug('Source is a liability account, destination is an asset account, return TRUE.');
return true;
}
// source is asset, destination is liability
if (in_array($destination->accountType->type, $sourceTypes, true) && AccountTypeEnum::ASSET->value === $source->accountType->type) {
Log::debug('Destination is a liability account, source is an asset account, return TRUE.');
return true;
}
return false;
}
private function isForeignCurrencyTransfer(TransactionJournal $journal): bool
{
if (TransactionTypeEnum::TRANSFER->value !== $journal->transactionType->type) {
return false;
}
/** @var Transaction $destination */
$destination = $journal->transactions()->where('amount', '>', 0)->first();
/** @var Transaction $source */
$source = $journal->transactions()->where('amount', '<', 0)->first();
// safety catch on NULL should not be necessary, we just had that catch.
// source amount = dest foreign amount
// source currency = dest foreign currency
// dest amount = source foreign currency
// dest currency = source foreign currency
// Log::debug(sprintf('[a] %s', bccomp(\FireflyIII\Support\Facades\Steam::positive($source->amount), \FireflyIII\Support\Facades\Steam::positive($destination->foreign_amount))));
// Log::debug(sprintf('[b] %s', bccomp(\FireflyIII\Support\Facades\Steam::positive($destination->amount), \FireflyIII\Support\Facades\Steam::positive($source->foreign_amount))));
// Log::debug(sprintf('[c] %s', var_export($source->transaction_currency_id === $destination->foreign_currency_id,true)));
// Log::debug(sprintf('[d] %s', var_export((int) $destination->transaction_currency_id ===(int) $source->foreign_currency_id, true)));
return
0 === bccomp(Steam::positive($source->amount), Steam::positive($destination->foreign_amount))
&& $source->transaction_currency_id === $destination->foreign_currency_id
&& 0 === bccomp(Steam::positive($destination->amount), Steam::positive($source->foreign_amount))
&& (int) $destination->transaction_currency_id === (int) $source->foreign_currency_id;
}
private function matchCurrencies(): void
{
$journals = TransactionJournal::leftJoin('transactions', 'transaction_journals.id', 'transactions.transaction_journal_id')->where(
'transactions.transaction_currency_id',
'!=',
DB::raw('transaction_journals.transaction_currency_id')
)->get(['transaction_journals.*']);
$count = 0;
/** @var TransactionJournal $journal */
foreach ($journals as $journal) {
if (!$this->isForeignCurrencyTransfer($journal) && !$this->isBetweenAssetAndLiability($journal)) {
Transaction::where('transaction_journal_id', $journal->id)->update(['transaction_currency_id' => $journal->transaction_currency_id]);
++$count;
continue;
}
Log::debug(sprintf('Can skip foreign currency transfer or transaction between asset and liability #%d.', $journal->id));
}
if (0 === $count) {
return;
}
$this->friendlyPositive(sprintf('Fixed %d journal(s) with mismatched currencies.', $journals->count()));
}
}

View File

@@ -42,31 +42,6 @@ class CreatesGroupMemberships extends Command
protected $description = 'Update group memberships';
protected $signature = 'correction:create-group-memberships';
/**
* Execute the console command.
*
* @throws FireflyException
*/
public function handle(): int
{
$this->createGroupMemberships();
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.
*
@@ -98,4 +73,29 @@ class CreatesGroupMemberships extends Command
$user->save();
}
}
/**
* Execute the console command.
*
* @throws FireflyException
*/
public function handle(): int
{
$this->createGroupMemberships();
return 0;
}
/**
* @throws FireflyException
*/
private function createGroupMemberships(): void
{
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
self::createGroupMembership($user);
}
}
}

View File

@@ -51,6 +51,30 @@ class RemovesEmptyJournals extends Command
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 {
/** @var null|TransactionJournal $journal */
$journal = TransactionJournal::find($entry->id);
$journal?->delete();
} catch (QueryException $e) {
Log::info(sprintf('Could not delete entry: %s', $e->getMessage()));
Log::error($e->getTraceAsString());
}
$this->friendlyInfo(sprintf('Deleted empty transaction journal #%d', $entry->id));
++$count;
}
}
/**
* Delete transactions and their journals if they have an uneven number of transactions.
*/
@@ -85,28 +109,4 @@ class RemovesEmptyJournals extends Command
}
}
}
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 {
/** @var null|TransactionJournal $journal */
$journal = TransactionJournal::find($entry->id);
$journal?->delete();
} catch (QueryException $e) {
Log::info(sprintf('Could not delete entry: %s', $e->getMessage()));
Log::error($e->getTraceAsString());
}
$this->friendlyInfo(sprintf('Deleted empty transaction journal #%d', $entry->id));
++$count;
}
}
}

View File

@@ -95,11 +95,19 @@ class RemovesLinksToDeletedObjects extends Command
$this->friendlyNeutral('Validated links to deleted objects.');
}
private function cleanupTags(array $tags): void
private function cleanupBudgets(array $budgets): void
{
$count = DB::table('tag_transaction_journal')->whereIn('tag_id', $tags)->delete();
$count = DB::table('budget_transaction_journal')->whereIn('budget_id', $budgets)->delete();
if ($count > 0) {
$this->friendlyInfo(sprintf('Removed %d old relationship(s) categories transactions and tags.', $count));
$this->friendlyInfo(sprintf('Removed %d old relationship(s) between budgets and transactions.', $count));
}
}
private function cleanupCategories(array $categories): void
{
$count = DB::table('category_transaction_journal')->whereIn('category_id', $categories)->delete();
if ($count > 0) {
$this->friendlyInfo(sprintf('Removed %d old relationship(s) categories categories and transactions.', $count));
}
}
@@ -127,19 +135,11 @@ class RemovesLinksToDeletedObjects extends Command
}
}
private function cleanupBudgets(array $budgets): void
private function cleanupTags(array $tags): void
{
$count = DB::table('budget_transaction_journal')->whereIn('budget_id', $budgets)->delete();
$count = DB::table('tag_transaction_journal')->whereIn('tag_id', $tags)->delete();
if ($count > 0) {
$this->friendlyInfo(sprintf('Removed %d old relationship(s) between budgets and transactions.', $count));
}
}
private function cleanupCategories(array $categories): void
{
$count = DB::table('category_transaction_journal')->whereIn('category_id', $categories)->delete();
if ($count > 0) {
$this->friendlyInfo(sprintf('Removed %d old relationship(s) categories categories and transactions.', $count));
$this->friendlyInfo(sprintf('Removed %d old relationship(s) categories transactions and tags.', $count));
}
}
}

View File

@@ -56,6 +56,27 @@ class RemovesOrphanedTransactions extends Command
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
/** @var null|TransactionJournal $journal */
$journal = TransactionJournal::find($transaction->transaction_journal_id);
$journal?->delete();
Transaction::where('transaction_journal_id', $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;
}
}
private function deleteOrphanedJournals(): void
{
$set = TransactionJournal::leftJoin('transaction_groups', 'transaction_journals.transaction_group_id', 'transaction_groups.id')
@@ -111,25 +132,4 @@ class RemovesOrphanedTransactions extends Command
}
}
}
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
/** @var null|TransactionJournal $journal */
$journal = TransactionJournal::find($transaction->transaction_journal_id);
$journal?->delete();
Transaction::where('transaction_journal_id', $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;
}
}
}

View File

@@ -48,6 +48,26 @@ class RestoresOAuthKeys extends Command
return 0;
}
private function generateKeys(): void
{
OAuthKeys::generateKeys();
}
private function keysInDatabase(): bool
{
return OAuthKeys::keysInDatabase();
}
private function keysOnDrive(): bool
{
return OAuthKeys::hasKeyFiles();
}
private function restoreKeysFromDB(): bool
{
return OAuthKeys::restoreKeysFromDB();
}
private function restoreOAuthKeys(): void
{
if (!$this->keysInDatabase() && !$this->keysOnDrive()) {
@@ -76,28 +96,8 @@ class RestoresOAuthKeys extends Command
}
}
private function keysInDatabase(): bool
{
return OAuthKeys::keysInDatabase();
}
private function keysOnDrive(): bool
{
return OAuthKeys::hasKeyFiles();
}
private function generateKeys(): void
{
OAuthKeys::generateKeys();
}
private function storeKeysInDB(): void
{
OAuthKeys::storeKeysInDB();
}
private function restoreKeysFromDB(): bool
{
return OAuthKeys::restoreKeysFromDB();
}
}

View File

@@ -43,6 +43,14 @@ class TriggersCreditCalculation extends Command
return 0;
}
private function processAccount(Account $account): void
{
/** @var CreditRecalculateService $object */
$object = app(CreditRecalculateService::class);
$object->setAccount($account);
$object->recalculate();
}
private function processAccounts(): void
{
$accounts = Account::leftJoin('account_types', 'accounts.account_type_id', 'account_types.id')->whereIn(
@@ -53,12 +61,4 @@ class TriggersCreditCalculation extends Command
$this->processAccount($account);
}
}
private function processAccount(Account $account): void
{
/** @var CreditRecalculateService $object */
$object = app(CreditRecalculateService::class);
$object->setAccount($account);
$object->recalculate();
}
}

View File

@@ -134,45 +134,55 @@ class ExportsData extends Command
}
/**
* 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
* @throws FilesystemException
*/
private function stupidLaravel(): void
private function exportData(array $options, array $data): void
{
$this->journalRepository = app(JournalRepositoryInterface::class);
$this->accountRepository = app(AccountRepositoryInterface::class);
$date = Carbon::now()->format('Y_m_d');
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));
}
}
/**
* @throws FireflyException
* @throws Exception
*/
private function parseOptions(): array
private function getAccountsParameter(): Collection
{
$start = $this->getDateParameter('start');
$end = $this->getDateParameter('end');
$accounts = $this->getAccountsParameter();
$export = $this->getExportDirectory();
$final = new Collection();
$accounts = new Collection();
$accountList = (string) $this->option('accounts');
$types = [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value];
if ('' !== $accountList) {
$accountIds = explode(',', $accountList);
$accounts = $this->accountRepository->getAccountsById($accountIds);
}
if ('' === $accountList) {
$accounts = $this->accountRepository->getAccountsByType($types);
}
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-subscriptions'),
'piggies' => $this->option('export-piggies'),
],
'start' => $start,
'end' => $end,
'accounts' => $accounts,
'directory' => $export,
'force' => $this->option('force'),
];
// 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;
}
/**
@@ -228,37 +238,6 @@ class ExportsData extends Command
return $date;
}
/**
* @throws FireflyException
*/
private function getAccountsParameter(): Collection
{
$final = new Collection();
$accounts = new Collection();
$accountList = (string) $this->option('accounts');
$types = [AccountTypeEnum::ASSET->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value];
if ('' !== $accountList) {
$accountIds = explode(',', $accountList);
$accounts = $this->accountRepository->getAccountsById($accountIds);
}
if ('' === $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;
}
/**
* @throws FireflyException
*/
@@ -277,22 +256,43 @@ class ExportsData extends Command
/**
* @throws FireflyException
* @throws FilesystemException
* @throws Exception
*/
private function exportData(array $options, array $data): void
private function parseOptions(): array
{
$date = Carbon::now()->format('Y_m_d');
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));
}
$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-subscriptions'),
'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.
*/
private function stupidLaravel(): void
{
$this->journalRepository = app(JournalRepositoryInterface::class);
$this->accountRepository = app(AccountRepositoryInterface::class);
}
}

View File

@@ -54,6 +54,45 @@ class ReportsEmptyObjects extends Command
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.
*/
@@ -110,43 +149,4 @@ class ReportsEmptyObjects extends Command
$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

@@ -55,6 +55,20 @@ class ValidatesEnvironmentVariables extends Command
return Command::SUCCESS;
}
private function validateGuard(): bool
{
$guard = config('auth.defaults.guard');
if ('web' !== $guard && 'remote_user_guard' !== $guard) {
$this->friendlyError(sprintf('AUTHENTICATION_GUARD "%s" is not a valid guard for Firefly III.', $guard));
$this->friendlyError('Please check your .env file and make sure you use a valid setting.');
$this->friendlyError('Valid guards are: web, remote_user_guard');
return false;
}
return true;
}
private function validateLanguage(): bool
{
$language = config('firefly.default_language');
@@ -80,20 +94,6 @@ class ValidatesEnvironmentVariables extends Command
return true;
}
private function validateGuard(): bool
{
$guard = config('auth.defaults.guard');
if ('web' !== $guard && 'remote_user_guard' !== $guard) {
$this->friendlyError(sprintf('AUTHENTICATION_GUARD "%s" is not a valid guard for Firefly III.', $guard));
$this->friendlyError('Please check your .env file and make sure you use a valid setting.');
$this->friendlyError('Valid guards are: web, remote_user_guard');
return false;
}
return true;
}
private function validateStaticToken(): bool
{
$token = (string) config('firefly.static_cron_token');

View File

@@ -39,16 +39,16 @@ trait ShowsFriendlyMessages
$this->friendlyNeutral($message);
}
public function friendlyNeutral(string $message): void
{
$this->line(sprintf(' [i] %s', trim($message)));
}
public function friendlyLine(string $message): void
{
$this->line(sprintf(' %s', trim($message)));
}
public function friendlyNeutral(string $message): void
{
$this->line(sprintf(' [i] %s', trim($message)));
}
public function friendlyPositive(string $message): void
{
$this->info(sprintf(' [✓] %s', trim($message)));

View File

@@ -112,19 +112,53 @@ class ForcesDecimalSize extends Command
return 0;
}
private function determineDatabaseType(): void
/**
* This method loops over all accounts and validates the amounts.
*/
private function correctAccountAmounts(TransactionCurrency $currency, array $fields): 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';
$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): void {
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;
}
if ('sqlite' === config('database.default')) {
$this->regularExpression = '"\.[\d]{%d}[1-9]+"';
/** @var Account $account */
foreach ($result as $account) {
/** @var string $field */
foreach ($fields as $field) {
$value = $account->{$field};
if (null === $value) {
continue;
}
// fix $field by rounding it down correctly.
$pow = 10 ** $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));
/** @var null|Account $updateAccount */
$updateAccount = Account::find($account->id);
$updateAccount?->update([$field => $correct]);
}
}
}
@@ -230,56 +264,6 @@ class ForcesDecimalSize extends Command
}
}
/**
* This method loops over all accounts and validates the amounts.
*/
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): void {
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) {
/** @var string $field */
foreach ($fields as $field) {
$value = $account->{$field};
if (null === $value) {
continue;
}
// fix $field by rounding it down correctly.
$pow = 10 ** $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));
/** @var null|Account $updateAccount */
$updateAccount = Account::find($account->id);
$updateAccount?->update([$field => $correct]);
}
}
}
/**
* This method fixes all auto budgets in currency $currency.
*/
@@ -328,6 +312,58 @@ class ForcesDecimalSize extends Command
}
}
/**
* This method fixes all piggy banks in currency $currency.
*/
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): void {
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) {
/** @var string $field */
foreach ($fields as $field) {
$value = $item->{$field};
if (null === $value) {
continue;
}
// fix $field by rounding it down correctly.
$pow = 10 ** $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));
/** @var null|PiggyBank $piggyBank */
$piggyBank = PiggyBank::find($item->id);
$piggyBank?->update([$field => $correct]);
}
}
}
/**
* This method fixes all piggy bank events in currency $currency.
*/
@@ -447,58 +483,6 @@ class ForcesDecimalSize extends Command
}
}
/**
* This method fixes all piggy banks in currency $currency.
*/
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): void {
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) {
/** @var string $field */
foreach ($fields as $field) {
$value = $item->{$field};
if (null === $value) {
continue;
}
// fix $field by rounding it down correctly.
$pow = 10 ** $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));
/** @var null|PiggyBank $piggyBank */
$piggyBank = PiggyBank::find($item->id);
$piggyBank?->update([$field => $correct]);
}
}
}
/**
* This method fixes all transactions in currency $currency.
*/
@@ -570,6 +554,22 @@ class ForcesDecimalSize 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]+"';
}
}
private function updateDecimals(): void
{
$this->friendlyInfo('Going to force the size of DECIMAL columns. Please hold.');

View File

@@ -55,110 +55,6 @@ class OutputsInstructions extends Command
return 0;
}
/**
* Render upgrade instructions.
*/
private function updateInstructions(): void
{
$version = (string) config('firefly.version');
/** @var array $config */
$config = config('upgrade.text.upgrade');
$text = '';
/** @var string $compare */
foreach (array_keys($config) as $compare) {
// if string starts with:
if (str_starts_with($version, $compare)) {
$text = (string) $config[$compare];
}
}
// validate some settings.
if ('' === $text && 'local' === (string) config('app.env')) {
$text = 'Please set APP_ENV=production for a safer environment.';
}
$prefix = 'v';
if (str_starts_with($version, 'develop') || str_starts_with($version, 'branch')) {
$prefix = '';
}
$this->newLine();
$this->showLogo();
$this->newLine();
$this->newLine();
$this->showLine();
$this->boxed('');
if ('' === $text) {
$this->boxed(sprintf('Thank you for updating to Firefly III, %s%s', $prefix, $version));
$this->boxedInfo('There are no extra upgrade instructions.');
$this->boxed('Firefly III should be ready for use.');
$this->boxed('');
$this->donationText();
$this->boxed('');
$this->showLine();
return;
}
$this->boxed(sprintf('Thank you for updating to Firefly III, %s%s!', $prefix, $version));
$this->boxedInfo($text);
$this->boxed('');
$this->donationText();
$this->boxed('');
$this->showLine();
}
/**
* The logo takes up 8 lines of code. So 8 colors can be used.
*/
private function showLogo(): void
{
$today = Carbon::now()->format('m-d');
$month = Carbon::now()->format('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'];
}
if ('ru_RU' === config('firefly.default_language')) {
$colors = ['blue', 'blue', 'blue', 'yellow', 'yellow', 'yellow', '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]));
$this->someQuote();
}
/**
* Show a line.
*/
private function showLine(): void
{
$this->line(sprintf('+%s+', str_repeat('-', 78)));
}
/**
* Show a nice box.
*/
@@ -242,6 +138,54 @@ class OutputsInstructions extends Command
$this->showLine();
}
/**
* Show a line.
*/
private function showLine(): void
{
$this->line(sprintf('+%s+', str_repeat('-', 78)));
}
/**
* The logo takes up 8 lines of code. So 8 colors can be used.
*/
private function showLogo(): void
{
$today = Carbon::now()->format('m-d');
$month = Carbon::now()->format('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'];
}
if ('ru_RU' === config('firefly.default_language')) {
$colors = ['blue', 'blue', 'blue', 'yellow', 'yellow', 'yellow', '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]));
$this->someQuote();
}
private function someQuote(): void
{
$lines = [
@@ -273,4 +217,60 @@ class OutputsInstructions extends Command
}
$this->line(sprintf(' %s', $lines[$random]));
}
/**
* Render upgrade instructions.
*/
private function updateInstructions(): void
{
$version = (string) config('firefly.version');
/** @var array $config */
$config = config('upgrade.text.upgrade');
$text = '';
/** @var string $compare */
foreach (array_keys($config) as $compare) {
// if string starts with:
if (str_starts_with($version, $compare)) {
$text = (string) $config[$compare];
}
}
// validate some settings.
if ('' === $text && 'local' === (string) config('app.env')) {
$text = 'Please set APP_ENV=production for a safer environment.';
}
$prefix = 'v';
if (str_starts_with($version, 'develop') || str_starts_with($version, 'branch')) {
$prefix = '';
}
$this->newLine();
$this->showLogo();
$this->newLine();
$this->newLine();
$this->showLine();
$this->boxed('');
if ('' === $text) {
$this->boxed(sprintf('Thank you for updating to Firefly III, %s%s', $prefix, $version));
$this->boxedInfo('There are no extra upgrade instructions.');
$this->boxed('Firefly III should be ready for use.');
$this->boxed('');
$this->donationText();
$this->boxed('');
$this->showLine();
return;
}
$this->boxed(sprintf('Thank you for updating to Firefly III, %s%s!', $prefix, $version));
$this->boxedInfo($text);
$this->boxed('');
$this->donationText();
$this->boxed('');
$this->showLine();
}
}

View File

@@ -140,6 +140,44 @@ class ApplyRules extends Command
return 0;
}
private function getRulesToApply(): Collection
{
Log::debug('getRulesToApply()');
$rulesToApply = new Collection();
/** @var RuleGroup $group */
foreach ($this->groups as $group) {
Log::debug(sprintf('Scanning rule group #%d', $group->id));
$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 ($test) {
Log::debug(sprintf('Will include rule #%d "%s"', $rule->id, $rule->title));
$rulesToApply->push($rule);
}
if (!$test) {
Log::debug(sprintf('Will not include rule #%d', $rule->id));
}
}
}
Log::debug(sprintf('Found %d rules to apply.', $rulesToApply->count()));
return $rulesToApply;
}
private function grabAllRules(): void
{
$this->groups = $this->ruleGroupRepository->getActiveGroups();
}
private function includeRule(Rule $rule, RuleGroup $group): bool
{
return in_array((int) $group->id, $this->ruleGroupSelection, true) || in_array((int) $rule->id, $this->ruleSelection, true) || $this->allRules;
}
/**
* 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
@@ -234,6 +272,47 @@ class ApplyRules extends Command
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 (!$inputEnd instanceof Carbon || null === $inputStart) {
Log::error('Could not parse start or end date in verifyInputDate().');
return;
}
if ($inputStart > $inputEnd) {
[$inputEnd, $inputStart] = [$inputStart, $inputEnd];
}
$this->startDate = $inputStart;
$this->endDate = $inputEnd;
}
private function verifyInputRuleGroups(): bool
{
$ruleGroupString = $this->option('rule_groups');
@@ -302,83 +381,4 @@ class ApplyRules extends Command
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 (!$inputEnd instanceof Carbon || null === $inputStart) {
Log::error('Could not parse start or end date in verifyInputDate().');
return;
}
if ($inputStart > $inputEnd) {
[$inputEnd, $inputStart] = [$inputStart, $inputEnd];
}
$this->startDate = $inputStart;
$this->endDate = $inputEnd;
}
private function grabAllRules(): void
{
$this->groups = $this->ruleGroupRepository->getActiveGroups();
}
private function getRulesToApply(): Collection
{
Log::debug('getRulesToApply()');
$rulesToApply = new Collection();
/** @var RuleGroup $group */
foreach ($this->groups as $group) {
Log::debug(sprintf('Scanning rule group #%d', $group->id));
$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 ($test) {
Log::debug(sprintf('Will include rule #%d "%s"', $rule->id, $rule->title));
$rulesToApply->push($rule);
}
if (!$test) {
Log::debug(sprintf('Will not include rule #%d', $rule->id));
}
}
}
Log::debug(sprintf('Found %d rules to apply.', $rulesToApply->count()));
return $rulesToApply;
}
private function includeRule(Rule $rule, RuleGroup $group): bool
{
return in_array((int) $group->id, $this->ruleGroupSelection, true) || in_array((int) $rule->id, $this->ruleSelection, true) || $this->allRules;
}
}

View File

@@ -142,6 +142,45 @@ class Cron extends Command
return 0;
}
private function autoBudgetCronJob(bool $force, ?Carbon $date): void
{
$autoBudget = new AutoBudgetCronjob();
$autoBudget->setForce($force);
// set date in cron job:
if ($date instanceof Carbon) {
$autoBudget->setDate($date);
}
$autoBudget->fire();
if ($autoBudget->jobErrored) {
$this->friendlyError(sprintf('Error in "create auto budgets" cron: %s', $autoBudget->message));
}
if ($autoBudget->jobFired) {
$this->friendlyInfo(sprintf('"Create auto budgets" cron fired: %s', $autoBudget->message));
}
if ($autoBudget->jobSucceeded) {
$this->friendlyPositive(sprintf('"Create auto budgets" cron ran with success: %s', $autoBudget->message));
}
}
private function checkForUpdates(bool $force): void
{
$updateCheck = new UpdateCheckCronjob();
$updateCheck->setForce($force);
$updateCheck->fire();
if ($updateCheck->jobErrored) {
$this->friendlyError(sprintf('Error in "update check" cron: %s', $updateCheck->message));
}
if ($updateCheck->jobFired) {
$this->friendlyInfo(sprintf('"Update check" cron fired: %s', $updateCheck->message));
}
if ($updateCheck->jobSucceeded) {
$this->friendlyPositive(sprintf('"Update check" cron ran with success: %s', $updateCheck->message));
}
}
private function exchangeRatesCronJob(bool $force, ?Carbon $date): void
{
Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__));
@@ -165,23 +204,6 @@ class Cron extends Command
}
}
private function checkForUpdates(bool $force): void
{
$updateCheck = new UpdateCheckCronjob();
$updateCheck->setForce($force);
$updateCheck->fire();
if ($updateCheck->jobErrored) {
$this->friendlyError(sprintf('Error in "update check" cron: %s', $updateCheck->message));
}
if ($updateCheck->jobFired) {
$this->friendlyInfo(sprintf('"Update check" cron fired: %s', $updateCheck->message));
}
if ($updateCheck->jobSucceeded) {
$this->friendlyPositive(sprintf('"Update check" cron ran with success: %s', $updateCheck->message));
}
}
/**
* @throws FireflyException
*/
@@ -207,28 +229,6 @@ class Cron extends Command
}
}
private function autoBudgetCronJob(bool $force, ?Carbon $date): void
{
$autoBudget = new AutoBudgetCronjob();
$autoBudget->setForce($force);
// set date in cron job:
if ($date instanceof Carbon) {
$autoBudget->setDate($date);
}
$autoBudget->fire();
if ($autoBudget->jobErrored) {
$this->friendlyError(sprintf('Error in "create auto budgets" cron: %s', $autoBudget->message));
}
if ($autoBudget->jobFired) {
$this->friendlyInfo(sprintf('"Create auto budgets" cron fired: %s', $autoBudget->message));
}
if ($autoBudget->jobSucceeded) {
$this->friendlyPositive(sprintf('"Create auto budgets" cron ran with success: %s', $autoBudget->message));
}
}
/**
* @throws FireflyException
*/

View File

@@ -86,6 +86,44 @@ class AddsTransactionIdentifiers extends Command
return 0;
}
private function findOpposing(Transaction $transaction, array $exclude): ?Transaction
{
// find opposing:
$amount = bcmul($transaction->amount, '-1');
try {
/** @var Transaction $opposing */
$opposing = Transaction::where('transaction_journal_id', $transaction->transaction_journal_id)
->where('amount', $amount)
->where('identifier', '=', 0)
->whereNotIn('id', $exclude)
->first()
;
} catch (QueryException $e) {
Log::error($e->getMessage());
$this->friendlyError('Firefly III could not find the "identifier" field in the "transactions" table.');
$this->friendlyError(sprintf('This field is required for Firefly III version %s to run.', config('firefly.version')));
$this->friendlyError('Please run "php artisan migrate --force" to add this field to the table.');
$this->friendlyError('Then, run "php artisan firefly:upgrade-database" to try again.');
return null;
}
return $opposing;
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
private function markAsExecuted(): void
{
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
@@ -97,13 +135,6 @@ class AddsTransactionIdentifiers extends Command
$this->count = 0;
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
/**
* 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.
@@ -130,35 +161,4 @@ class AddsTransactionIdentifiers extends Command
++$identifier;
}
}
private function findOpposing(Transaction $transaction, array $exclude): ?Transaction
{
// find opposing:
$amount = bcmul($transaction->amount, '-1');
try {
/** @var Transaction $opposing */
$opposing = Transaction::where('transaction_journal_id', $transaction->transaction_journal_id)
->where('amount', $amount)
->where('identifier', '=', 0)
->whereNotIn('id', $exclude)
->first()
;
} catch (QueryException $e) {
Log::error($e->getMessage());
$this->friendlyError('Firefly III could not find the "identifier" field in the "transactions" table.');
$this->friendlyError(sprintf('This field is required for Firefly III version %s to run.', config('firefly.version')));
$this->friendlyError('Please run "php artisan migrate --force" to add this field to the table.');
$this->friendlyError('Then, run "php artisan firefly:upgrade-database" to try again.');
return null;
}
return $opposing;
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
}

View File

@@ -75,34 +75,6 @@ class RemovesDatabaseDecryption extends Command
return 0;
}
private function decryptTable(string $table, array $fields): void
{
if ($this->isDecrypted($table)) {
return;
}
foreach ($fields as $field) {
$this->decryptField($table, $field);
}
$this->friendlyPositive(sprintf('Decrypted the data in table "%s".', $table));
// mark as decrypted:
$configName = sprintf('is_decrypted_%s', $table);
FireflyConfig::set($configName, true);
}
private function isDecrypted(string $table): bool
{
$configName = sprintf('is_decrypted_%s', $table);
$configVar = null;
try {
$configVar = FireflyConfig::get($configName, false);
} catch (FireflyException $e) {
Log::error($e->getMessage());
}
return (bool) $configVar?->data;
}
private function decryptField(string $table, string $field): void
{
$rows = DB::table($table)->get(['id', $field]);
@@ -113,6 +85,29 @@ class RemovesDatabaseDecryption extends Command
}
}
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);
Log::warning($message);
Log::warning($value);
Log::warning($e->getTraceAsString());
return;
}
/** @var null|Preference $object */
$object = Preference::find($id);
if (null !== $object) {
$object->data = $newValue;
$object->save();
}
}
private function decryptRow(string $table, string $field, stdClass $row): void
{
$original = $row->{$field};
@@ -143,6 +138,34 @@ class RemovesDatabaseDecryption extends Command
}
}
private function decryptTable(string $table, array $fields): void
{
if ($this->isDecrypted($table)) {
return;
}
foreach ($fields as $field) {
$this->decryptField($table, $field);
}
$this->friendlyPositive(sprintf('Decrypted the data in table "%s".', $table));
// mark as decrypted:
$configName = sprintf('is_decrypted_%s', $table);
FireflyConfig::set($configName, true);
}
private function isDecrypted(string $table): bool
{
$configName = sprintf('is_decrypted_%s', $table);
$configVar = null;
try {
$configVar = FireflyConfig::get($configName, false);
} catch (FireflyException $e) {
Log::error($e->getMessage());
}
return (bool) $configVar?->data;
}
/**
* Tries to decrypt data. Will only throw an exception when the MAC is invalid.
*
@@ -164,27 +187,4 @@ class RemovesDatabaseDecryption extends Command
return $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);
Log::warning($message);
Log::warning($value);
Log::warning($e->getTraceAsString());
return;
}
/** @var null|Preference $object */
$object = Preference::find($id);
if (null !== $object) {
$object->data = $newValue;
$object->save();
}
}
}

View File

@@ -58,6 +58,11 @@ class RepairsAccountBalances extends Command
return 0;
}
private function correctBalanceAmounts(): void
{
AccountBalanceCalculator::recalculateAll(false);
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
@@ -69,9 +74,4 @@ class RepairsAccountBalances extends Command
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function correctBalanceAmounts(): void
{
AccountBalanceCalculator::recalculateAll(false);
}
}

View File

@@ -73,6 +73,18 @@ class UpgradesAccountCurrencies extends Command
return 0;
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
private function markAsExecuted(): void
{
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
@@ -85,35 +97,6 @@ class UpgradesAccountCurrencies extends Command
$this->count = 0;
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
private function updateAccountCurrencies(): void
{
$users = $this->userRepos->all();
foreach ($users as $user) {
$this->updateCurrenciesForUser($user);
}
}
private function updateCurrenciesForUser(User $user): void
{
$this->accountRepos->setUser($user);
$accounts = $this->accountRepos->getAccountsByType([AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value]);
// get user's currency preference:
$primaryCurrency = Amount::getPrimaryCurrencyByUserGroup($user->userGroup);
/** @var Account $account */
foreach ($accounts as $account) {
$this->updateAccount($account, $primaryCurrency);
}
}
private function updateAccount(Account $account, TransactionCurrency $currency): void
{
$this->accountRepos->setUser($account->user);
@@ -153,8 +136,25 @@ class UpgradesAccountCurrencies extends Command
}
}
private function markAsExecuted(): void
private function updateAccountCurrencies(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
$users = $this->userRepos->all();
foreach ($users as $user) {
$this->updateCurrenciesForUser($user);
}
}
private function updateCurrenciesForUser(User $user): void
{
$this->accountRepos->setUser($user);
$accounts = $this->accountRepos->getAccountsByType([AccountTypeEnum::DEFAULT->value, AccountTypeEnum::ASSET->value]);
// get user's currency preference:
$primaryCurrency = Amount::getPrimaryCurrencyByUserGroup($user->userGroup);
/** @var Account $account */
foreach ($accounts as $account) {
$this->updateAccount($account, $primaryCurrency);
}
}
}

View File

@@ -84,20 +84,6 @@ class UpgradesBillsToRules extends Command
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);
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
@@ -105,34 +91,9 @@ class UpgradesBillsToRules extends Command
return (bool) $configVar?->data;
}
/**
* Migrate bills to new rule structure for a specific user.
*/
private function migrateUser(User $user): void
private function markAsExecuted(): void
{
$this->ruleGroupRepository->setUser($user);
$this->billRepository->setUser($user);
$this->ruleRepository->setUser($user);
/** @var Preference $lang */
$lang = Preferences::getForUser($user, 'language', 'en_US');
$language = null !== $lang->data && !is_array($lang->data) ? (string) $lang->data : 'en_US';
$groupTitle = (string) trans('firefly.rulegroup_for_bills_title', [], $language);
$ruleGroup = $this->ruleGroupRepository->findByTitle($groupTitle);
if (!$ruleGroup instanceof RuleGroup) {
$ruleGroup = $this->ruleGroupRepository->store([
'title' => (string) trans('firefly.rulegroup_for_bills_title', [], $language),
'description' => (string) trans('firefly.rulegroup_for_bills_description', [], $language),
'active' => true,
]);
}
$bills = $this->billRepository->getBills();
/** @var Bill $bill */
foreach ($bills as $bill) {
$this->migrateBill($ruleGroup, $bill, $lang);
}
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function migrateBill(RuleGroup $ruleGroup, Bill $bill, Preference $language): void
@@ -183,8 +144,47 @@ class UpgradesBillsToRules extends Command
++$this->count;
}
private function markAsExecuted(): void
/**
* Migrate bills to new rule structure for a specific user.
*/
private function migrateUser(User $user): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
$this->ruleGroupRepository->setUser($user);
$this->billRepository->setUser($user);
$this->ruleRepository->setUser($user);
/** @var Preference $lang */
$lang = Preferences::getForUser($user, 'language', 'en_US');
$language = null !== $lang->data && !is_array($lang->data) ? (string) $lang->data : 'en_US';
$groupTitle = (string) trans('firefly.rulegroup_for_bills_title', [], $language);
$ruleGroup = $this->ruleGroupRepository->findByTitle($groupTitle);
if (!$ruleGroup instanceof RuleGroup) {
$ruleGroup = $this->ruleGroupRepository->store([
'title' => (string) trans('firefly.rulegroup_for_bills_title', [], $language),
'description' => (string) trans('firefly.rulegroup_for_bills_description', [], $language),
'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

@@ -57,23 +57,6 @@ class UpgradesBudgetLimitPeriods extends Command
return 0;
}
private function isExecuted(): bool
{
$configVar = 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);
}
}
private function fixLimit(BudgetLimit $limit): void
{
$period = $this->getLimitPeriod($limit);
@@ -91,7 +74,7 @@ class UpgradesBudgetLimitPeriods extends Command
return;
}
$limit->period = $period;
$limit->save();
$limit->saveQuietly();
$msg = sprintf(
'Budget limit #%d (%s - %s) period is "%s".',
@@ -155,8 +138,25 @@ class UpgradesBudgetLimitPeriods extends Command
return null;
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar->data;
}
private function markAsExecuted(): void
{
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

@@ -70,7 +70,7 @@ class UpgradesBudgetLimits extends Command
if (null !== $user) {
$currency = Amount::getPrimaryCurrencyByUserGroup($user->userGroup);
$budgetLimit->transaction_currency_id = $currency->id;
$budgetLimit->save();
$budgetLimit->saveQuietly();
$this->friendlyInfo(sprintf(
'Budget limit #%d (part of budget "%s") now has a currency setting (%s).',
$budgetLimit->id,

View File

@@ -64,6 +64,24 @@ class UpgradesCurrencyPreferences extends Command
return 0;
}
private function getPreference(User $user): string
{
$preference = Preference::where('user_id', $user->id)
->where('name', 'currencyPreference')
->first(['id', 'user_id', 'name', 'data', 'updated_at', 'created_at'])
;
if (null === $preference) {
return 'EUR';
}
if (null !== $preference->data && !is_array($preference->data)) {
return (string) $preference->data;
}
return 'EUR';
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
@@ -71,6 +89,11 @@ class UpgradesCurrencyPreferences extends Command
return (bool) $configVar?->data;
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function runUpgrade(): void
{
$groups = UserGroup::get();
@@ -126,27 +149,4 @@ class UpgradesCurrencyPreferences extends Command
$user->currencies()->updateExistingPivot($primaryCurrency->id, ['user_default' => true]);
$user->userGroup->currencies()->updateExistingPivot($primaryCurrency->id, ['group_default' => true]);
}
private function getPreference(User $user): string
{
$preference = Preference::where('user_id', $user->id)
->where('name', 'currencyPreference')
->first(['id', 'user_id', 'name', 'data', 'updated_at', 'created_at'])
;
if (null === $preference) {
return 'EUR';
}
if (null !== $preference->data && !is_array($preference->data)) {
return (string) $preference->data;
}
return 'EUR';
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
}

View File

@@ -68,11 +68,48 @@ class UpgradesJournalMetaData extends Command
return 0;
}
private function isMigrated(): bool
private function getIdsForBudgets(): array
{
$configVar = FireflyConfig::get(UpgradesToGroups::CONFIG_NAME, false);
$transactions = DB::table('budget_transaction')
->distinct()
->pluck('transaction_id')
->toArray()
;
$array = [];
$chunks = array_chunk($transactions, 500);
return (bool) $configVar->data;
foreach ($chunks as $chunk) {
$set = DB::table('transactions')
->whereIn('transactions.id', $chunk)
->pluck('transaction_journal_id')
->toArray()
;
$array = array_merge($array, $set);
}
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;
}
private function isExecuted(): bool
@@ -82,6 +119,18 @@ class UpgradesJournalMetaData extends Command
return (bool) $configVar->data;
}
private function isMigrated(): bool
{
$configVar = FireflyConfig::get(UpgradesToGroups::CONFIG_NAME, false);
return (bool) $configVar->data;
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function migrateAll(): void
{
$this->migrateBudgets();
@@ -108,28 +157,6 @@ class UpgradesJournalMetaData extends Command
}
}
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;
}
private function migrateBudgetsForJournal(TransactionJournal $journal): void
{
// grab category from first transaction
@@ -179,28 +206,6 @@ class UpgradesJournalMetaData extends Command
}
}
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;
}
private function migrateCategoriesForJournal(TransactionJournal $journal): void
{
// grab category from first transaction
@@ -229,9 +234,4 @@ class UpgradesJournalMetaData extends Command
$journal->categories()->sync([$category->id]);
}
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
}

View File

@@ -61,6 +61,36 @@ class UpgradesLiabilities extends Command
return 0;
}
private function correctOpeningBalance(Account $account, TransactionJournal $openingBalance): void
{
$source = $this->getSourceTransaction($openingBalance);
$destination = $this->getDestinationTransaction($openingBalance);
if (!$source instanceof Transaction || !$destination instanceof Transaction) {
return;
}
// source MUST be the liability.
if ($destination->account_id === $account->id) {
// so if not, switch things around:
$sourceAccountId = $source->account_id;
$source->account_id = $destination->account_id;
$destination->account_id = $sourceAccountId;
$source->save();
$destination->save();
}
}
private function getDestinationTransaction(TransactionJournal $journal): ?Transaction
{
/** @var null|Transaction */
return $journal->transactions()->where('amount', '>', 0)->first();
}
private function getSourceTransaction(TransactionJournal $journal): ?Transaction
{
/** @var null|Transaction */
return $journal->transactions()->where('amount', '<', 0)->first();
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
@@ -68,14 +98,9 @@ class UpgradesLiabilities extends Command
return (bool) $configVar?->data;
}
private function upgradeLiabilities(): void
private function markAsExecuted(): void
{
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
$this->upgradeForUser($user);
}
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function upgradeForUser(User $user): void
@@ -96,6 +121,16 @@ class UpgradesLiabilities extends Command
}
}
private function upgradeLiabilities(): void
{
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
$this->upgradeForUser($user);
}
}
private function upgradeLiability(Account $account): void
{
/** @var AccountRepositoryInterface $repository */
@@ -117,39 +152,4 @@ class UpgradesLiabilities extends Command
$factory->crud($account, 'liability_direction', 'debit');
}
}
private function correctOpeningBalance(Account $account, TransactionJournal $openingBalance): void
{
$source = $this->getSourceTransaction($openingBalance);
$destination = $this->getDestinationTransaction($openingBalance);
if (!$source instanceof Transaction || !$destination instanceof Transaction) {
return;
}
// source MUST be the liability.
if ($destination->account_id === $account->id) {
// so if not, switch things around:
$sourceAccountId = $source->account_id;
$source->account_id = $destination->account_id;
$destination->account_id = $sourceAccountId;
$source->save();
$destination->save();
}
}
private function getSourceTransaction(TransactionJournal $journal): ?Transaction
{
/** @var null|Transaction */
return $journal->transactions()->where('amount', '<', 0)->first();
}
private function getDestinationTransaction(TransactionJournal $journal): ?Transaction
{
/** @var null|Transaction */
return $journal->transactions()->where('amount', '>', 0)->first();
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
}

View File

@@ -63,59 +63,40 @@ class UpgradesLiabilitiesEight extends Command
return 0;
}
private function isExecuted(): bool
private function deleteCreditTransaction(Account $account): void
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
private function upgradeLiabilities(): void
{
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
$this->upgradeForUser($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.*'])
$liabilityType = TransactionType::whereType(TransactionTypeEnum::LIABILITY_CREDIT->value)->first();
$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.*'])
;
/** @var Account $account */
foreach ($accounts as $account) {
$this->upgradeLiability($account);
$service = app(CreditRecalculateService::class);
$service->setAccount($account);
$service->recalculate();
if (null !== $liabilityJournal && null !== $liabilityJournal->transactionGroup) {
$group = $liabilityJournal->transactionGroup;
$service = new TransactionGroupDestroyService();
$service->destroy($group);
}
}
private function upgradeLiability(Account $account): void
private function deleteTransactions(Account $account): int
{
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$repository->setUser($account->user);
$count = 0;
$journals = TransactionJournal::leftJoin('transactions', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')->where(
'transactions.account_id',
$account->id
)->get(['transaction_journals.*']);
$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));
$service = app(TransactionGroupDestroyService::class);
/** @var TransactionJournal $journal */
foreach ($journals as $journal) {
if (null !== $journal->transactionGroup) {
$service->destroy($journal->transactionGroup);
++$count;
}
}
return $count;
}
private function hasBadOpening(Account $account): bool
@@ -142,19 +123,16 @@ class UpgradesLiabilitiesEight extends Command
return (bool) $openingJournal->date->isSameDay($liabilityJournal->date);
}
private function deleteCreditTransaction(Account $account): void
private function isExecuted(): bool
{
$liabilityType = TransactionType::whereType(TransactionTypeEnum::LIABILITY_CREDIT->value)->first();
$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 && null !== $liabilityJournal->transactionGroup) {
$group = $liabilityJournal->transactionGroup;
$service = new TransactionGroupDestroyService();
$service->destroy($group);
}
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function reverseOpeningBalance(Account $account): void
@@ -186,29 +164,51 @@ class UpgradesLiabilitiesEight extends Command
Log::warning('Did not find opening balance.');
}
private function deleteTransactions(Account $account): int
private function upgradeForUser(User $user): void
{
$count = 0;
$journals = TransactionJournal::leftJoin('transactions', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')->where(
'transactions.account_id',
$account->id
)->get(['transaction_journals.*']);
$accounts = $user
->accounts()
->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id')
->whereIn('account_types.type', config('firefly.valid_liabilities'))
->get(['accounts.*'])
;
$service = app(TransactionGroupDestroyService::class);
/** @var Account $account */
foreach ($accounts as $account) {
$this->upgradeLiability($account);
$service = app(CreditRecalculateService::class);
$service->setAccount($account);
$service->recalculate();
}
}
/** @var TransactionJournal $journal */
foreach ($journals as $journal) {
if (null !== $journal->transactionGroup) {
$service->destroy($journal->transactionGroup);
++$count;
private function upgradeLiabilities(): void
{
$users = User::get();
/** @var User $user */
foreach ($users as $user) {
$this->upgradeForUser($user);
}
}
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));
}
}
return $count;
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
}

View File

@@ -70,17 +70,9 @@ class UpgradesMultiPiggyBanks extends Command
return (bool) $configVar?->data;
}
private function upgradePiggyBanks(): void
private function markAsExecuted(): void
{
$this->repository = app(PiggyBankRepositoryInterface::class);
$this->accountRepository = app(AccountRepositoryInterface::class);
$set = PiggyBank::whereNotNull('account_id')->get();
Log::debug(sprintf('Will update %d piggy banks(s).', $set->count()));
/** @var PiggyBank $piggyBank */
foreach ($set as $piggyBank) {
$this->upgradePiggyBank($piggyBank);
}
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function upgradePiggyBank(PiggyBank $piggyBank): void
@@ -109,8 +101,16 @@ class UpgradesMultiPiggyBanks extends Command
$piggyBank->piggyBankRepetitions()->delete();
}
private function markAsExecuted(): void
private function upgradePiggyBanks(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
$this->repository = app(PiggyBankRepositoryInterface::class);
$this->accountRepository = app(AccountRepositoryInterface::class);
$set = PiggyBank::whereNotNull('account_id')->get();
Log::debug(sprintf('Will update %d piggy banks(s).', $set->count()));
/** @var PiggyBank $piggyBank */
foreach ($set as $piggyBank) {
$this->upgradePiggyBank($piggyBank);
}
}
}

View File

@@ -71,18 +71,9 @@ class UpgradesRecurrenceMetaData extends Command
return (bool) $configVar?->data;
}
private function migrateMetaData(): int
private function markAsExecuted(): void
{
$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;
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function migrateEntry(RecurrenceMeta $meta): int
@@ -109,8 +100,17 @@ class UpgradesRecurrenceMetaData extends Command
return 1;
}
private function markAsExecuted(): void
private function migrateMetaData(): int
{
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;
}
}

View File

@@ -68,6 +68,11 @@ class UpgradesRuleActions extends Command
return (bool) $configVar?->data;
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function replaceEqualSign(): void
{
$count = 0;
@@ -179,9 +184,4 @@ class UpgradesRuleActions extends Command
}
}
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
}

View File

@@ -56,6 +56,11 @@ class UpgradesTagLocations extends Command
return 0;
}
private function hasLocationDetails(Tag $tag): bool
{
return !in_array(null, [$tag->latitude, $tag->longitude, $tag->zoomLevel], true);
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
@@ -63,21 +68,9 @@ class UpgradesTagLocations extends Command
return (bool) $configVar?->data;
}
private function migrateTagLocations(): void
private function markAsExecuted(): void
{
$tags = Tag::get();
/** @var Tag $tag */
foreach ($tags as $tag) {
if ($this->hasLocationDetails($tag)) {
$this->migrateLocationDetails($tag);
}
}
}
private function hasLocationDetails(Tag $tag): bool
{
return !in_array(null, [$tag->latitude, $tag->longitude, $tag->zoomLevel], true);
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function migrateLocationDetails(Tag $tag): void
@@ -95,8 +88,15 @@ class UpgradesTagLocations extends Command
$tag->save();
}
private function markAsExecuted(): void
private function migrateTagLocations(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
$tags = Tag::get();
/** @var Tag $tag */
foreach ($tags as $tag) {
if ($this->hasLocationDetails($tag)) {
$this->migrateLocationDetails($tag);
}
}
}
}

View File

@@ -83,100 +83,18 @@ class UpgradesToGroups extends Command
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
private function findOpposingTransaction(TransactionJournal $journal, Transaction $transaction): ?Transaction
{
$this->count = 0;
$this->journalRepository = app(JournalRepositoryInterface::class);
$this->service = app(JournalDestroyService::class);
$this->groupFactory = app(TransactionGroupFactory::class);
$this->cliRepository = app(JournalCLIRepositoryInterface::class);
}
$set = $journal->transactions->filter(static function (Transaction $subject) use ($transaction): bool {
$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)));
private function isMigrated(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return $amount && $identifier;
});
return (bool) $configVar?->data;
}
/**
* @throws Exception
*/
private function makeGroupsFromSplitJournals(): void
{
$splitJournals = $this->cliRepository->getSplitJournals();
if ($splitJournals->count() > 0) {
$this->friendlyLine(sprintf('Going to convert %d split transaction(s). Please hold..', $splitJournals->count()));
/** @var TransactionJournal $journal */
foreach ($splitJournals as $journal) {
$this->makeMultiGroup($journal);
}
}
}
/**
* @throws Exception
*/
private function makeMultiGroup(TransactionJournal $journal): void
{
// double check transaction count.
if ($journal->transactions->count() <= 2) {
Log::debug(sprintf('Will not try to convert journal #%d because it has 2 or fewer transactions.', $journal->id));
return;
}
Log::debug(sprintf('Will now try to convert journal #%d', $journal->id));
$this->journalRepository->setUser($journal->user);
$this->groupFactory->setUser($journal->user);
$this->cliRepository->setUser($journal->user);
$data = [
// mandatory fields.
'group_title' => $journal->description,
'transactions' => [],
];
$destTransactions = $this->getDestinationTransactions($journal);
Log::debug(sprintf('Will use %d positive transactions to create a new group.', $destTransactions->count()));
/** @var Transaction $transaction */
foreach ($destTransactions as $transaction) {
$data['transactions'][] = $this->generateTransaction($journal, $transaction);
}
Log::debug(sprintf('Now calling transaction journal factory (%d transactions in array)', count($data['transactions'])));
$group = $this->groupFactory->create($data);
Log::debug('Done calling transaction journal factory');
// delete the old transaction journal.
$this->service->destroy($journal);
++$this->count;
// report on result:
Log::debug(sprintf(
'Migrated journal #%d into group #%d with these journals: #%s',
$journal->id,
$group->id,
implode(', #', $group->transactionJournals->pluck('id')->toArray())
));
$this->friendlyInfo(sprintf(
'Migrated journal #%d into group #%d with these journals: #%s',
$journal->id,
$group->id,
implode(', #', $group->transactionJournals->pluck('id')->toArray())
));
}
private function getDestinationTransactions(TransactionJournal $journal): Collection
{
return $journal->transactions->filter(static fn (Transaction $transaction): bool => $transaction->amount > 0);
return $set->first();
}
/**
@@ -268,18 +186,9 @@ class UpgradesToGroups extends Command
];
}
private function findOpposingTransaction(TransactionJournal $journal, Transaction $transaction): ?Transaction
private function getDestinationTransactions(TransactionJournal $journal): Collection
{
$set = $journal->transactions->filter(static function (Transaction $subject) use ($transaction): bool {
$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();
return $journal->transactions->filter(static fn (Transaction $transaction): bool => $transaction->amount > 0);
}
private function getTransactionBudget(Transaction $left, Transaction $right): ?int
@@ -336,6 +245,25 @@ class UpgradesToGroups extends Command
return null;
}
private function giveGroup(array $array): void
{
$groupId = DB::table('transaction_groups')->insertGetId([
'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
'updated_at' => Carbon::now()->format('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;
}
private function isMigrated(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
/**
* Gives all journals without a group a group.
*/
@@ -354,20 +282,92 @@ class UpgradesToGroups extends Command
}
}
private function giveGroup(array $array): void
/**
* @throws Exception
*/
private function makeGroupsFromSplitJournals(): void
{
$groupId = DB::table('transaction_groups')->insertGetId([
'created_at' => Carbon::now()->format('Y-m-d H:i:s'),
'updated_at' => Carbon::now()->format('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]);
$splitJournals = $this->cliRepository->getSplitJournals();
if ($splitJournals->count() > 0) {
$this->friendlyLine(sprintf('Going to convert %d split transaction(s). Please hold..', $splitJournals->count()));
/** @var TransactionJournal $journal */
foreach ($splitJournals as $journal) {
$this->makeMultiGroup($journal);
}
}
}
/**
* @throws Exception
*/
private function makeMultiGroup(TransactionJournal $journal): void
{
// double check transaction count.
if ($journal->transactions->count() <= 2) {
Log::debug(sprintf('Will not try to convert journal #%d because it has 2 or fewer transactions.', $journal->id));
return;
}
Log::debug(sprintf('Will now try to convert journal #%d', $journal->id));
$this->journalRepository->setUser($journal->user);
$this->groupFactory->setUser($journal->user);
$this->cliRepository->setUser($journal->user);
$data = [
// mandatory fields.
'group_title' => $journal->description,
'transactions' => [],
];
$destTransactions = $this->getDestinationTransactions($journal);
Log::debug(sprintf('Will use %d positive transactions to create a new group.', $destTransactions->count()));
/** @var Transaction $transaction */
foreach ($destTransactions as $transaction) {
$data['transactions'][] = $this->generateTransaction($journal, $transaction);
}
Log::debug(sprintf('Now calling transaction journal factory (%d transactions in array)', count($data['transactions'])));
$group = $this->groupFactory->create($data);
Log::debug('Done calling transaction journal factory');
// delete the old transaction journal.
$this->service->destroy($journal);
++$this->count;
// report on result:
Log::debug(sprintf(
'Migrated journal #%d into group #%d with these journals: #%s',
$journal->id,
$group->id,
implode(', #', $group->transactionJournals->pluck('id')->toArray())
));
$this->friendlyInfo(sprintf(
'Migrated journal #%d into group #%d with these journals: #%s',
$journal->id,
$group->id,
implode(', #', $group->transactionJournals->pluck('id')->toArray())
));
}
private function markAsMigrated(): void
{
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

@@ -79,17 +79,303 @@ class UpgradesTransferCurrencies extends Command
}
/**
* 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.
* The destination transaction must have the correct currency. If not, it will be set by
* taking it from the destination account's preference.
*/
private function stupidLaravel(): void
private function fixDestinationUnmatchedCurrency(): void
{
$this->count = 0;
$this->accountRepos = app(AccountRepositoryInterface::class);
$this->cliRepos = app(JournalCLIRepositoryInterface::class);
$this->accountCurrencies = [];
$this->resetInformation();
if (
$this->destinationCurrency instanceof TransactionCurrency
&& null === $this->destinationTransaction->foreign_amount
&& (int) $this->destinationTransaction->transaction_currency_id !== $this->destinationCurrency->id
) {
$message = sprintf(
'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.',
$this->destinationTransaction->id,
$this->destinationTransaction->transaction_currency_id,
$this->destinationAccount->id,
$this->destinationTransaction->amount
);
$this->friendlyWarning($message);
++$this->count;
$this->destinationTransaction->transaction_currency_id = $this->destinationCurrency->id;
$this->destinationTransaction->save();
}
}
/**
* The destination transaction must have a currency. If not, it will be added by
* taking it from the destination account's preference.
*/
private function fixDestNoCurrency(): void
{
if (null === $this->destinationTransaction->transaction_currency_id && $this->destinationCurrency instanceof TransactionCurrency) {
$this->destinationTransaction->transaction_currency_id = $this->destinationCurrency->id;
$message = sprintf(
'Transaction #%d has no currency setting, now set to %s.',
$this->destinationTransaction->id,
$this->destinationCurrency->code
);
$this->friendlyInfo($message);
++$this->count;
$this->destinationTransaction->save();
}
}
/**
* 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($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
));
}
}
/**
* If the destination account currency is the same as the source currency,
* both foreign_amount and foreign_currency_id fields must be NULL
* for both transactions (because foreign currency info would not make sense)
*/
private function fixInvalidForeignCurrency(): void
{
if ($this->destinationCurrency->id === $this->sourceCurrency->id) {
// update both transactions to match:
$this->sourceTransaction->foreign_amount = null;
$this->sourceTransaction->foreign_currency_id = null;
$this->destinationTransaction->foreign_amount = null;
$this->destinationTransaction->foreign_currency_id = null;
$this->sourceTransaction->save();
$this->destinationTransaction->save();
}
}
/**
* If destination account currency is different from source account currency,
* then both transactions must get the source account's currency as normal currency
* and the opposing account's currency as foreign currency.
*/
private function fixMismatchedForeignCurrency(): void
{
if ($this->sourceCurrency->id !== $this->destinationCurrency->id) {
$this->sourceTransaction->transaction_currency_id = $this->sourceCurrency->id;
$this->sourceTransaction->foreign_currency_id = $this->destinationCurrency->id;
$this->destinationTransaction->transaction_currency_id = $this->sourceCurrency->id;
$this->destinationTransaction->foreign_currency_id = $this->destinationCurrency->id;
$this->sourceTransaction->save();
$this->destinationTransaction->save();
++$this->count;
$this->friendlyInfo(sprintf(
'Verified foreign currency ID of transaction #%d and #%d',
$this->sourceTransaction->id,
$this->destinationTransaction->id
));
}
}
/**
* 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 && $this->sourceCurrency instanceof TransactionCurrency) {
$this->sourceTransaction->transaction_currency_id = $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
* to restore it.
*/
private function fixSourceNullForeignAmount(): void
{
if (null === $this->sourceTransaction->foreign_amount && null !== $this->destinationTransaction->foreign_amount) {
$this->sourceTransaction->foreign_amount = bcmul($this->destinationTransaction->foreign_amount, '-1');
$this->sourceTransaction->save();
++$this->count;
$this->friendlyInfo(sprintf(
'Restored foreign amount of source transaction #%d to %s',
$this->sourceTransaction->id,
$this->sourceTransaction->foreign_amount
));
}
}
/**
* 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 (
$this->sourceCurrency instanceof TransactionCurrency
&& null === $this->sourceTransaction->foreign_amount
&& (int) $this->sourceTransaction->transaction_currency_id !== $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 = $this->sourceCurrency->id;
$this->sourceTransaction->save();
}
}
/**
* This method makes sure that the transaction journal uses the currency given in the source transaction.
*/
private function fixTransactionJournalCurrency(TransactionJournal $journal): void
{
if ((int) $journal->transaction_currency_id !== $this->sourceCurrency->id) {
$oldCurrencyCode = $journal->transactionCurrency->code ?? '(nothing)';
$journal->transaction_currency_id = $this->sourceCurrency->id;
$message = sprintf(
'Transfer #%d ("%s") has been updated to use %s instead of %s.',
$journal->id,
$journal->description,
$this->sourceCurrency->code,
$oldCurrencyCode
);
++$this->count;
$this->friendlyInfo($message);
$journal->save();
}
}
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 (!$currency instanceof TransactionCurrency) {
$this->accountCurrencies[$accountId] = 0;
return null;
}
$this->accountCurrencies[$accountId] = $currency;
return $currency;
}
/**
* Extract destination transaction, destination account + destination account currency from the journal.
*/
private function getDestinationInformation(TransactionJournal $journal): void
{
$this->destinationTransaction = $this->getDestinationTransaction($journal);
$this->destinationAccount = $this->destinationTransaction?->account;
$this->destinationCurrency = $this->destinationAccount instanceof Account ? $this->getCurrency($this->destinationAccount) : null;
}
private function getDestinationTransaction(TransactionJournal $transfer): ?Transaction
{
/** @var null|Transaction */
return $transfer->transactions()->where('amount', '>', 0)->first();
}
/**
* Extract source transaction, source account + source account currency from the journal.
*/
private function getSourceInformation(TransactionJournal $journal): void
{
$this->sourceTransaction = $this->getSourceTransaction($journal);
$this->sourceAccount = $this->sourceTransaction?->account;
$this->sourceCurrency = $this->sourceAccount instanceof Account ? $this->getCurrency($this->sourceAccount) : null;
}
private function getSourceTransaction(TransactionJournal $transfer): ?Transaction
{
/** @var null|Transaction */
return $transfer->transactions()->where('amount', '<', 0)->first();
}
/**
* Is either the source or destination transaction NULL?
*/
private function isEmptyTransactions(): bool
{
return
!$this->sourceTransaction instanceof Transaction
|| !$this->destinationTransaction instanceof Transaction
|| !$this->sourceAccount instanceof Account
|| !$this->destinationAccount instanceof Account;
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
private function isNoCurrencyPresent(): bool
{
// source account must have a currency preference.
if (!$this->sourceCurrency instanceof TransactionCurrency) {
$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 (!$this->destinationCurrency instanceof TransactionCurrency) {
$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?
*/
private function isSplitJournal(TransactionJournal $transfer): bool
{
return $transfer->transactions->count() > 2;
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
/**
@@ -105,13 +391,6 @@ class UpgradesTransferCurrencies extends Command
$this->destinationCurrency = null;
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
/**
* 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
@@ -130,6 +409,20 @@ class UpgradesTransferCurrencies extends Command
}
}
/**
* 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();
}
private function updateTransferCurrency(TransactionJournal $transfer): void
{
$this->resetInformation();
@@ -187,297 +480,4 @@ class UpgradesTransferCurrencies extends Command
// fix journal itself:
$this->fixTransactionJournalCurrency($transfer);
}
/**
* Is this a split transaction journal?
*/
private function isSplitJournal(TransactionJournal $transfer): bool
{
return $transfer->transactions->count() > 2;
}
/**
* Extract source transaction, source account + source account currency from the journal.
*/
private function getSourceInformation(TransactionJournal $journal): void
{
$this->sourceTransaction = $this->getSourceTransaction($journal);
$this->sourceAccount = $this->sourceTransaction?->account;
$this->sourceCurrency = $this->sourceAccount instanceof Account ? $this->getCurrency($this->sourceAccount) : null;
}
private function getSourceTransaction(TransactionJournal $transfer): ?Transaction
{
/** @var null|Transaction */
return $transfer->transactions()->where('amount', '<', 0)->first();
}
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 (!$currency instanceof TransactionCurrency) {
$this->accountCurrencies[$accountId] = 0;
return null;
}
$this->accountCurrencies[$accountId] = $currency;
return $currency;
}
/**
* Extract destination transaction, destination account + destination account currency from the journal.
*/
private function getDestinationInformation(TransactionJournal $journal): void
{
$this->destinationTransaction = $this->getDestinationTransaction($journal);
$this->destinationAccount = $this->destinationTransaction?->account;
$this->destinationCurrency = $this->destinationAccount instanceof Account ? $this->getCurrency($this->destinationAccount) : null;
}
private function getDestinationTransaction(TransactionJournal $transfer): ?Transaction
{
/** @var null|Transaction */
return $transfer->transactions()->where('amount', '>', 0)->first();
}
/**
* Is either the source or destination transaction NULL?
*/
private function isEmptyTransactions(): bool
{
return
!$this->sourceTransaction instanceof Transaction
|| !$this->destinationTransaction instanceof Transaction
|| !$this->sourceAccount instanceof Account
|| !$this->destinationAccount instanceof Account;
}
private function isNoCurrencyPresent(): bool
{
// source account must have a currency preference.
if (!$this->sourceCurrency instanceof TransactionCurrency) {
$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 (!$this->destinationCurrency instanceof TransactionCurrency) {
$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 && $this->sourceCurrency instanceof TransactionCurrency) {
$this->sourceTransaction->transaction_currency_id = $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 (
$this->sourceCurrency instanceof TransactionCurrency
&& null === $this->sourceTransaction->foreign_amount
&& (int) $this->sourceTransaction->transaction_currency_id !== $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 = $this->sourceCurrency->id;
$this->sourceTransaction->save();
}
}
/**
* The destination transaction must have a currency. If not, it will be added by
* taking it from the destination account's preference.
*/
private function fixDestNoCurrency(): void
{
if (null === $this->destinationTransaction->transaction_currency_id && $this->destinationCurrency instanceof TransactionCurrency) {
$this->destinationTransaction->transaction_currency_id = $this->destinationCurrency->id;
$message = sprintf(
'Transaction #%d has no currency setting, now set to %s.',
$this->destinationTransaction->id,
$this->destinationCurrency->code
);
$this->friendlyInfo($message);
++$this->count;
$this->destinationTransaction->save();
}
}
/**
* The destination transaction must have the correct currency. If not, it will be set by
* taking it from the destination account's preference.
*/
private function fixDestinationUnmatchedCurrency(): void
{
if (
$this->destinationCurrency instanceof TransactionCurrency
&& null === $this->destinationTransaction->foreign_amount
&& (int) $this->destinationTransaction->transaction_currency_id !== $this->destinationCurrency->id
) {
$message = sprintf(
'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.',
$this->destinationTransaction->id,
$this->destinationTransaction->transaction_currency_id,
$this->destinationAccount->id,
$this->destinationTransaction->amount
);
$this->friendlyWarning($message);
++$this->count;
$this->destinationTransaction->transaction_currency_id = $this->destinationCurrency->id;
$this->destinationTransaction->save();
}
}
/**
* If the destination account currency is the same as the source currency,
* both foreign_amount and foreign_currency_id fields must be NULL
* for both transactions (because foreign currency info would not make sense)
*/
private function fixInvalidForeignCurrency(): void
{
if ($this->destinationCurrency->id === $this->sourceCurrency->id) {
// update both transactions to match:
$this->sourceTransaction->foreign_amount = null;
$this->sourceTransaction->foreign_currency_id = null;
$this->destinationTransaction->foreign_amount = null;
$this->destinationTransaction->foreign_currency_id = null;
$this->sourceTransaction->save();
$this->destinationTransaction->save();
}
}
/**
* If destination account currency is different from source account currency,
* then both transactions must get the source account's currency as normal currency
* and the opposing account's currency as foreign currency.
*/
private function fixMismatchedForeignCurrency(): void
{
if ($this->sourceCurrency->id !== $this->destinationCurrency->id) {
$this->sourceTransaction->transaction_currency_id = $this->sourceCurrency->id;
$this->sourceTransaction->foreign_currency_id = $this->destinationCurrency->id;
$this->destinationTransaction->transaction_currency_id = $this->sourceCurrency->id;
$this->destinationTransaction->foreign_currency_id = $this->destinationCurrency->id;
$this->sourceTransaction->save();
$this->destinationTransaction->save();
++$this->count;
$this->friendlyInfo(sprintf(
'Verified foreign currency ID of transaction #%d and #%d',
$this->sourceTransaction->id,
$this->destinationTransaction->id
));
}
}
/**
* 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.
*/
private function fixSourceNullForeignAmount(): void
{
if (null === $this->sourceTransaction->foreign_amount && null !== $this->destinationTransaction->foreign_amount) {
$this->sourceTransaction->foreign_amount = bcmul($this->destinationTransaction->foreign_amount, '-1');
$this->sourceTransaction->save();
++$this->count;
$this->friendlyInfo(sprintf(
'Restored foreign amount of source transaction #%d to %s',
$this->sourceTransaction->id,
$this->sourceTransaction->foreign_amount
));
}
}
/**
* 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($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
));
}
}
/**
* This method makes sure that the transaction journal uses the currency given in the source transaction.
*/
private function fixTransactionJournalCurrency(TransactionJournal $journal): void
{
if ((int) $journal->transaction_currency_id !== $this->sourceCurrency->id) {
$oldCurrencyCode = $journal->transactionCurrency->code ?? '(nothing)';
$journal->transaction_currency_id = $this->sourceCurrency->id;
$message = sprintf(
'Transfer #%d ("%s") has been updated to use %s instead of %s.',
$journal->id,
$journal->description,
$this->sourceCurrency->code,
$oldCurrencyCode
);
++$this->count;
$this->friendlyInfo($message);
$journal->save();
}
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
}

View File

@@ -72,6 +72,99 @@ class UpgradesVariousCurrencyInformation extends Command
return 0;
}
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 (!$currency instanceof TransactionCurrency) {
$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.
*/
private function getLeadTransaction(TransactionJournal $journal): ?Transaction
{
/** @var null|Transaction $lead */
$lead = null;
switch ($journal->transactionType->type) {
default:
break;
case TransactionTypeEnum::WITHDRAWAL->value:
$lead = $journal->transactions()->where('amount', '<', 0)->first();
break;
case TransactionTypeEnum::DEPOSIT->value:
$lead = $journal->transactions()->where('amount', '>', 0)->first();
break;
case TransactionTypeEnum::OPENING_BALANCE->value:
// 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', '!=', AccountTypeEnum::INITIAL_BALANCE->value)
->first(['transactions.*'])
;
break;
case TransactionTypeEnum::RECONCILIATION->value:
// 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', '!=', AccountTypeEnum::RECONCILIATION->value)
->first(['transactions.*'])
;
break;
}
return $lead;
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
private function isMultiCurrency(Account $account): bool
{
$value = $this->accountRepos->getMetaValue($account, 'is_multi_currency');
if (null === $value) {
return false;
}
return '1' === $value;
}
private function markAsExecuted(): void
{
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
@@ -86,34 +179,6 @@ class UpgradesVariousCurrencyInformation extends Command
$this->cliRepos = app(JournalCLIRepositoryInterface::class);
}
private function isExecuted(): bool
{
$configVar = FireflyConfig::get(self::CONFIG_NAME, false);
return (bool) $configVar?->data;
}
/**
* 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 updateOtherJournalsCurrencies(): void
{
$set = $this->cliRepos->getAllJournals([
TransactionTypeEnum::WITHDRAWAL->value,
TransactionTypeEnum::DEPOSIT->value,
TransactionTypeEnum::OPENING_BALANCE->value,
TransactionTypeEnum::RECONCILIATION->value,
]);
/** @var TransactionJournal $journal */
foreach ($set as $journal) {
$this->updateJournalCurrency($journal);
}
}
private function updateJournalCurrency(TransactionJournal $journal): void
{
$this->accountRepos->setUser($journal->user);
@@ -166,88 +231,23 @@ class UpgradesVariousCurrencyInformation extends Command
}
/**
* Gets the transaction that determines the transaction that "leads" and will determine
* the currency to be used by all transactions, and the journal itself.
* 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 getLeadTransaction(TransactionJournal $journal): ?Transaction
private function updateOtherJournalsCurrencies(): void
{
/** @var null|Transaction $lead */
$lead = null;
$set = $this->cliRepos->getAllJournals([
TransactionTypeEnum::WITHDRAWAL->value,
TransactionTypeEnum::DEPOSIT->value,
TransactionTypeEnum::OPENING_BALANCE->value,
TransactionTypeEnum::RECONCILIATION->value,
]);
switch ($journal->transactionType->type) {
default:
break;
case TransactionTypeEnum::WITHDRAWAL->value:
$lead = $journal->transactions()->where('amount', '<', 0)->first();
break;
case TransactionTypeEnum::DEPOSIT->value:
$lead = $journal->transactions()->where('amount', '>', 0)->first();
break;
case TransactionTypeEnum::OPENING_BALANCE->value:
// 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', '!=', AccountTypeEnum::INITIAL_BALANCE->value)
->first(['transactions.*'])
;
break;
case TransactionTypeEnum::RECONCILIATION->value:
// 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', '!=', AccountTypeEnum::RECONCILIATION->value)
->first(['transactions.*'])
;
break;
/** @var TransactionJournal $journal */
foreach ($set as $journal) {
$this->updateJournalCurrency($journal);
}
return $lead;
}
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 (!$currency instanceof TransactionCurrency) {
$this->accountCurrencies[$accountId] = 0;
return null;
}
$this->accountCurrencies[$accountId] = $currency;
return $currency;
}
private function isMultiCurrency(Account $account): bool
{
$value = $this->accountRepos->getMetaValue($account, 'is_multi_currency');
if (null === $value) {
return false;
}
return '1' === $value;
}
private function markAsExecuted(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
}
}

View File

@@ -69,18 +69,9 @@ class UpgradesWebhooks extends Command
return (bool) $configVar?->data;
}
private function upgradeWebhooks(): void
private function markAsExecuted(): void
{
$set = Webhook::where('delivery', '>', 1)
->orWhere('trigger', '>', 1)
->orWhere('response', '>', 1)
->get()
;
/** @var Webhook $webhook */
foreach ($set as $webhook) {
$this->upgradeWebhook($webhook);
}
FireflyConfig::set(self::CONFIG_NAME, true);
}
private function upgradeWebhook(Webhook $webhook): void
@@ -111,8 +102,17 @@ class UpgradesWebhooks extends Command
$this->friendlyPositive(sprintf('Webhook #%d upgraded.', $webhook->id));
}
private function markAsExecuted(): void
private function upgradeWebhooks(): void
{
FireflyConfig::set(self::CONFIG_NAME, true);
$set = Webhook::where('delivery', '>', 1)
->orWhere('trigger', '>', 1)
->orWhere('response', '>', 1)
->get()
;
/** @var Webhook $webhook */
foreach ($set as $webhook) {
$this->upgradeWebhook($webhook);
}
}
}

View File

@@ -37,6 +37,15 @@ use Illuminate\Support\Facades\Log;
*/
trait VerifiesAccessToken
{
/**
* Abstract method to make sure trait knows about method "option".
*
* @param null|string $key
*
* @return mixed
*/
abstract public function option($key = null);
/**
* @throws FireflyException
*/
@@ -54,15 +63,6 @@ trait VerifiesAccessToken
return $user;
}
/**
* Abstract method to make sure trait knows about method "option".
*
* @param null|string $key
*
* @return mixed
*/
abstract public function option($key = null);
/**
* Returns false when given token does not match given user token.
*/

View File

@@ -1,8 +1,10 @@
<?php
declare(strict_types=1);
/*
* UpdatedAccount.php
* Copyright (c) 2021 james@firefly-iii.org
* CreatedBudget.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -20,24 +22,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Events\Model\Budget;
namespace FireflyIII\Events;
use FireflyIII\Models\Account;
use FireflyIII\Events\Event;
use FireflyIII\Models\Budget;
use Illuminate\Queue\SerializesModels;
/**
* Class UpdatedAccount
*/
class UpdatedAccount extends Event
class CreatedBudget extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*/
public function __construct(
public Account $account
public Budget $budget,
public bool $createWebhookMessages
) {}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* DestroyedBudget.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Events\Model\Budget;
use FireflyIII\Events\Event;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class DestroyedBudget extends Event
{
use SerializesModels;
public function __construct()
{
Log::debug('Created event DestroyedBudget');
}
}

View File

@@ -3,8 +3,8 @@
declare(strict_types=1);
/*
* TriggeredStoredTransactionGroup.php
* Copyright (c) 2025 james@firefly-iii.org
* DestroyingBudget.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -22,29 +22,20 @@ declare(strict_types=1);
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Events\Model\TransactionGroup;
namespace FireflyIII\Events\Model\Budget;
use FireflyIII\Events\Event;
use FireflyIII\Models\RuleGroup;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\Budget;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
/**
* @deprecated
*/
class TriggeredStoredTransactionGroup extends Event
class DestroyingBudget extends Event
{
use SerializesModels;
public ?RuleGroup $ruleGroup = null;
/**
* Create a new event instance.
*/
public function __construct(
public TransactionGroup $transactionGroup,
?RuleGroup $ruleGroup = null
public Budget $budget
) {
$this->ruleGroup = $ruleGroup;
Log::debug(sprintf('Created event DestroyingBudget(#%d)', $budget->id));
}
}

View File

@@ -1,8 +1,10 @@
<?php
declare(strict_types=1);
/*
* TriggeredAuditLog.php
* Copyright (c) 2022 james@firefly-iii.org
* UpdatedBudget.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -20,30 +22,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Events\Model\Budget;
namespace FireflyIII\Events;
use Illuminate\Database\Eloquent\Model;
use FireflyIII\Events\Event;
use FireflyIII\Models\Budget;
use Illuminate\Queue\SerializesModels;
/**
* Class TriggeredAuditLog
*/
class TriggeredAuditLog extends Event
class UpdatedBudget extends Event
{
use SerializesModels;
/**
* Create a new event instance.
*
* @SuppressWarnings("PHPMD.ExcessiveParameterList")
*/
public function __construct(
public Model $changer,
public Model $auditable,
public string $field,
public mixed $before,
public mixed $after
public Budget $budget,
public bool $createWebhookMessages
) {}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* CreatedBudgetLimit.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Events\Model\BudgetLimit;
use FireflyIII\Events\Event;
use FireflyIII\Models\BudgetLimit;
use Illuminate\Queue\SerializesModels;
class CreatedBudgetLimit extends Event
{
use SerializesModels;
public function __construct(
public BudgetLimit $budgetLimit,
public bool $createWebhookMessages
) {}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/*
* DestroyedBudgetLimit.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Events\Model\BudgetLimit;
use Carbon\Carbon;
use FireflyIII\Events\Event;
use FireflyIII\Models\Budget;
use FireflyIII\User;
use Illuminate\Queue\SerializesModels;
class DestroyedBudgetLimit extends Event
{
use SerializesModels;
public function __construct(
public User $user,
public Budget $budget,
public Carbon $start,
public Carbon $end,
public bool $createWebhookMessages
) {}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* UpdatedBudgetLimit.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Events\Model\BudgetLimit;
use FireflyIII\Events\Event;
use FireflyIII\Models\BudgetLimit;
use Illuminate\Queue\SerializesModels;
class UpdatedBudgetLimit extends Event
{
use SerializesModels;
public function __construct(
public BudgetLimit $budgetLimit,
public bool $createWebhookMessages
) {}
}

View File

@@ -25,9 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Events\Model\TransactionGroup;
use FireflyIII\Events\Event;
use FireflyIII\Models\TransactionGroup;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class CreatedSingleTransactionGroup extends Event
{
@@ -37,9 +35,7 @@ class CreatedSingleTransactionGroup extends Event
* Create a new event instance.
*/
public function __construct(
public TransactionGroup $transactionGroup,
public TransactionGroupEventFlags $flags
) {
Log::debug(__METHOD__);
}
public TransactionGroupEventFlags $flags,
public TransactionGroupEventObjects $objects
) {}
}

View File

@@ -25,7 +25,6 @@ declare(strict_types=1);
namespace FireflyIII\Events\Model\TransactionGroup;
use FireflyIII\Events\Event;
use FireflyIII\Models\TransactionGroup;
use Illuminate\Queue\SerializesModels;
class DestroyedSingleTransactionGroup extends Event
@@ -36,6 +35,7 @@ class DestroyedSingleTransactionGroup extends Event
* Create a new event instance.
*/
public function __construct(
public TransactionGroup $transactionGroup
public TransactionGroupEventFlags $flags,
public TransactionGroupEventObjects $objects
) {}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Events\Model\TransactionGroup;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* This class collects all objects before and after the creation, removal or updating
* of a transaction group. The idea is that this class contains all relevant objects.
* Right now, that means journals, tags, accounts, budgets and categories.
*
* By collecting these objects (in case of an update: before AND after update) there
* is a unified set of objects to manage: update balances, recalculate credits, etc.
*/
class TransactionGroupEventObjects
{
public Collection $accounts;
public Collection $budgets;
public Collection $categories;
public Collection $tags;
public Collection $transactionGroups;
public Collection $transactionJournals;
public function __construct()
{
$this->accounts = new Collection();
$this->budgets = new Collection();
$this->categories = new Collection();
$this->tags = new Collection();
$this->transactionGroups = new Collection();
$this->transactionJournals = new Collection();
}
public static function collectFromTransactionGroup(TransactionGroup $transactionGroup): self
{
Log::debug(sprintf('collectFromTransactionGroup(#%d)', $transactionGroup->id));
$object = new self();
$object->appendFromTransactionGroup($transactionGroup);
return $object;
}
public function appendFromTransactionGroup(TransactionGroup $transactionGroup): void
{
$this->transactionGroups->push($transactionGroup);
/** @var TransactionJournal $journal */
foreach ($transactionGroup->transactionJournals as $journal) {
$this->transactionJournals->push($journal);
$this->budgets = $this->budgets->merge($journal->budgets);
$this->categories = $this->categories->merge($journal->categories);
$this->tags = $this->tags->merge($journal->tags);
/** @var Transaction $transaction */
foreach ($journal->transactions as $transaction) {
$this->accounts->push($transaction->account);
}
}
$this->transactionGroups = $this->transactionGroups->unique('id');
$this->transactionJournals = $this->transactionJournals->unique('id');
$this->budgets = $this->budgets->unique('id');
$this->categories = $this->categories->unique('id');
$this->tags = $this->tags->unique('id');
$this->accounts = $this->accounts->unique('id');
}
}

View File

@@ -25,7 +25,6 @@ declare(strict_types=1);
namespace FireflyIII\Events\Model\TransactionGroup;
use FireflyIII\Events\Event;
use FireflyIII\Models\TransactionGroup;
use Illuminate\Queue\SerializesModels;
class UpdatedSingleTransactionGroup extends Event
@@ -36,7 +35,7 @@ class UpdatedSingleTransactionGroup extends Event
* Create a new event instance.
*/
public function __construct(
public TransactionGroup $transactionGroup,
public TransactionGroupEventFlags $flags
public TransactionGroupEventFlags $flags,
public TransactionGroupEventObjects $objects
) {}
}

View File

@@ -181,46 +181,6 @@ class GracefulNotFoundHandler extends ExceptionHandler
return redirect(route('accounts.index', [$shortType]));
}
/**
* @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();
$param = $route->parameter('transactionGroup');
$groupId = is_object($param) ? 0 : (int) $param;
/** @var null|TransactionGroup $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 null|TransactionJournal $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 (TransactionTypeEnum::RECONCILIATION->value === $type) {
return redirect(route('accounts.index', ['asset']));
}
return redirect(route('transactions.index', [strtolower((string) $type)]));
}
/**
* @return Response
*
@@ -265,4 +225,44 @@ class GracefulNotFoundHandler extends ExceptionHandler
return parent::render($request, $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();
$param = $route->parameter('transactionGroup');
$groupId = is_object($param) ? 0 : (int) $param;
/** @var null|TransactionGroup $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 null|TransactionJournal $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 (TransactionTypeEnum::RECONCILIATION->value === $type) {
return redirect(route('accounts.index', ['asset']));
}
return redirect(route('transactions.index', [strtolower((string) $type)]));
}
}

View File

@@ -260,11 +260,6 @@ class Handler extends ExceptionHandler
parent::report($e);
}
private function shouldntReportLocal(Throwable $e): bool
{
return null !== Arr::first($this->dontReport, static fn ($type): bool => $e instanceof $type);
}
/**
* Convert a validation exception into a response.
*
@@ -298,4 +293,9 @@ class Handler extends ExceptionHandler
return null !== $previousHost && $previousHost === $safeHost ? $previous : $safe;
}
private function shouldntReportLocal(Throwable $e): bool
{
return null !== Arr::first($this->dontReport, static fn ($type): bool => $e instanceof $type);
}
}

View File

@@ -67,6 +67,43 @@ class AccountFactory
$this->validFields = config('firefly.valid_account_fields');
}
/**
* @throws FireflyException
*/
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 ($return instanceof Account) {
return $return;
}
$return = $this->createAccount($type, $data);
event(new CreatedNewAccount($return));
return $return;
}
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 null|Account */
return $this->user
->accounts()
->where('account_type_id', $type->id)
->where('name', $accountName)
->first()
;
}
/**
* @throws FireflyException
*/
@@ -104,27 +141,10 @@ class AccountFactory
return $return;
}
/**
* @throws FireflyException
*/
public function create(array $data): Account
public function setUser(User $user): void
{
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 ($return instanceof Account) {
return $return;
}
$return = $this->createAccount($type, $data);
event(new CreatedNewAccount($return));
return $return;
$this->user = $user;
$this->accountRepository->setUser($user);
}
/**
@@ -160,18 +180,28 @@ class AccountFactory
return $result;
}
public function find(string $accountName, string $accountType): ?Account
/**
* @throws FireflyException
*/
private function cleanMetaDataArray(Account $account, array $data): array
{
Log::debug(sprintf('Now in AccountFactory::find("%s", "%s")', $accountName, $accountType));
$type = AccountType::whereType($accountType)->first();
$currencyId = array_key_exists('currency_id', $data) ? (int) $data['currency_id'] : 0;
$currencyCode = array_key_exists('currency_code', $data) ? (string) $data['currency_code'] : '';
$accountRole = array_key_exists('account_role', $data) ? (string) $data['account_role'] : null;
$currency = $this->getCurrency($currencyId, $currencyCode);
/** @var null|Account */
return $this->user
->accounts()
->where('account_type_id', $type->id)
->where('name', $accountName)
->first()
;
// only asset account may have a role:
if (AccountTypeEnum::ASSET->value !== $account->accountType->type) {
$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;
}
/**
@@ -245,25 +275,27 @@ class AccountFactory
/**
* @throws FireflyException
*/
private function cleanMetaDataArray(Account $account, array $data): array
private function storeCreditLiability(Account $account, array $data): void
{
$currencyId = array_key_exists('currency_id', $data) ? (int) $data['currency_id'] : 0;
$currencyCode = array_key_exists('currency_code', $data) ? (string) $data['currency_code'] : '';
$accountRole = array_key_exists('account_role', $data) ? (string) $data['account_role'] : null;
$currency = $this->getCurrency($currencyId, $currencyCode);
// only asset account may have a role:
if (AccountTypeEnum::ASSET->value !== $account->accountType->type) {
$accountRole = '';
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);
}
}
// 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;
}
private function storeMetaData(Account $account, array $data): void
@@ -324,32 +356,6 @@ class AccountFactory
}
}
/**
* @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);
}
}
}
/**
* @throws FireflyException
*/
@@ -370,10 +376,4 @@ class AccountFactory
$updateService->setUser($account->user);
$updateService->update($account, ['order' => $order]);
}
public function setUser(User $user): void
{
$this->user = $user;
$this->accountRepository->setUser($user);
}
}

View File

@@ -33,6 +33,11 @@ use FireflyIII\Support\Facades\Steam;
*/
class AccountMetaFactory
{
public function create(array $data): ?AccountMeta
{
return AccountMeta::create($data);
}
/**
* Create update or delete meta data.
*/
@@ -63,9 +68,4 @@ class AccountMetaFactory
return $entry;
}
public function create(array $data): ?AccountMeta
{
return AccountMeta::create($data);
}
}

View File

@@ -36,6 +36,16 @@ class CategoryFactory
{
private User $user;
public function findByName(string $name): ?Category
{
/** @var null|Category */
return $this->user
->categories()
->where('name', $name)
->first()
;
}
/**
* @throws FireflyException
*/
@@ -77,16 +87,6 @@ class CategoryFactory
return null;
}
public function findByName(string $name): ?Category
{
/** @var null|Category */
return $this->user
->categories()
->where('name', $name)
->first()
;
}
public function setUser(User $user): void
{
$this->user = $user;

View File

@@ -59,93 +59,6 @@ class PiggyBankFactory
$this->piggyBankRepository = app(PiggyBankRepositoryInterface::class);
}
public function setUser(User $user): void
{
$this->user = $user;
$this->currencyRepository->setUser($user);
$this->accountRepository->setUser($user);
$this->piggyBankRepository->setUser($user);
}
/**
* Store a piggy bank or come back with an exception.
*/
public function store(array $data): PiggyBank
{
$piggyBankData = $data;
// unset some fields
unset(
$piggyBankData['object_group_title'],
$piggyBankData['transaction_currency_code'],
$piggyBankData['transaction_currency_id'],
$piggyBankData['accounts'],
$piggyBankData['object_group_id'],
$piggyBankData['notes']
);
// validate amount:
if (array_key_exists('target_amount', $piggyBankData) && '' === (string) $piggyBankData['target_amount']) {
$piggyBankData['target_amount'] = '0';
}
$piggyBankData['start_date_tz'] = $piggyBankData['start_date']?->format('e');
$piggyBankData['target_date_tz'] = $piggyBankData['target_date']?->format('e');
$piggyBankData['account_id'] = null;
$piggyBankData['transaction_currency_id'] = $this->getCurrency($data)->id;
$piggyBankData['order'] = 131337;
try {
/** @var PiggyBank $piggyBank */
$piggyBank = PiggyBank::createQuietly($piggyBankData);
} catch (QueryException $e) {
Log::error(sprintf('Could not store piggy bank: %s', $e->getMessage()), $piggyBankData);
throw new FireflyException('400005: Could not store new piggy bank.', 0, $e);
}
$piggyBank = $this->setOrder($piggyBank, $data);
$this->linkToAccountIds($piggyBank, $data['accounts']);
$this->piggyBankRepository->updateNote($piggyBank, $data['notes']);
$objectGroupTitle = $data['object_group_title'] ?? '';
if ('' !== $objectGroupTitle) {
$objectGroup = $this->findOrCreateObjectGroup($objectGroupTitle);
if ($objectGroup instanceof ObjectGroup) {
$piggyBank->objectGroups()->sync([$objectGroup->id]);
}
}
// try also with ID
$objectGroupId = (int) ($data['object_group_id'] ?? 0);
if (0 !== $objectGroupId) {
$objectGroup = $this->findObjectGroupById($objectGroupId);
if ($objectGroup instanceof ObjectGroup) {
$piggyBank->objectGroups()->sync([$objectGroup->id]);
}
}
Log::debug('Touch piggy bank');
$piggyBank->encrypted = false;
$piggyBank->save();
$piggyBank->touch();
return $piggyBank;
}
private function getCurrency(array $data): TransactionCurrency
{
// currency:
$primaryCurrency = Amount::getPrimaryCurrency();
$currency = null;
if (array_key_exists('transaction_currency_code', $data)) {
$currency = $this->currencyRepository->findByCode((string) ($data['transaction_currency_code'] ?? ''));
}
if (array_key_exists('transaction_currency_id', $data)) {
$currency = $this->currencyRepository->find((int) ($data['transaction_currency_id'] ?? 0));
}
$currency ??= $primaryCurrency;
return $currency;
}
public function find(?int $piggyBankId, ?string $piggyBankName): ?PiggyBank
{
$piggyBankId = (int) $piggyBankId;
@@ -188,45 +101,6 @@ class PiggyBankFactory
;
}
private function setOrder(PiggyBank $piggyBank, array $data): PiggyBank
{
$this->resetOrder();
$order = $this->getMaxOrder() + 1;
if (array_key_exists('order', $data)) {
$order = $data['order'];
}
$piggyBank->order = $order;
$piggyBank->saveQuietly();
return $piggyBank;
}
public function resetOrder(): void
{
// TODO duplicate code
$set = PiggyBank::leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id')
->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id')
->where('accounts.user_id', $this->user->id)
->with(['objectGroups'])
->orderBy('piggy_banks.order', 'ASC')
->get(['piggy_banks.*'])
;
$current = 1;
foreach ($set as $piggyBank) {
if ($piggyBank->order !== $current) {
Log::debug(sprintf('Piggy bank #%d ("%s") was at place %d but should be on %d', $piggyBank->id, $piggyBank->name, $piggyBank->order, $current));
$piggyBank->order = $current;
$piggyBank->save();
}
++$current;
}
}
private function getMaxOrder(): int
{
return (int) $this->piggyBankRepository->getPiggyBanks()->max('order');
}
public function linkToAccountIds(PiggyBank $piggyBank, array $accounts): void
{
Log::debug(sprintf('Linking piggy bank #%d to %d accounts.', $piggyBank->id, count($accounts)), $accounts);
@@ -316,4 +190,130 @@ class PiggyBankFactory
Log::warning('No accounts to link to piggy bank, will not change whatever is there now.');
}
}
public function resetOrder(): void
{
// TODO duplicate code
$set = PiggyBank::leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id')
->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id')
->where('accounts.user_id', $this->user->id)
->with(['objectGroups'])
->orderBy('piggy_banks.order', 'ASC')
->get(['piggy_banks.*'])
;
$current = 1;
foreach ($set as $piggyBank) {
if ($piggyBank->order !== $current) {
Log::debug(sprintf('Piggy bank #%d ("%s") was at place %d but should be on %d', $piggyBank->id, $piggyBank->name, $piggyBank->order, $current));
$piggyBank->order = $current;
$piggyBank->save();
}
++$current;
}
}
public function setUser(User $user): void
{
$this->user = $user;
$this->currencyRepository->setUser($user);
$this->accountRepository->setUser($user);
$this->piggyBankRepository->setUser($user);
}
/**
* Store a piggy bank or come back with an exception.
*/
public function store(array $data): PiggyBank
{
$piggyBankData = $data;
// unset some fields
unset(
$piggyBankData['object_group_title'],
$piggyBankData['transaction_currency_code'],
$piggyBankData['transaction_currency_id'],
$piggyBankData['accounts'],
$piggyBankData['object_group_id'],
$piggyBankData['notes']
);
// validate amount:
if (array_key_exists('target_amount', $piggyBankData) && '' === (string) $piggyBankData['target_amount']) {
$piggyBankData['target_amount'] = '0';
}
$piggyBankData['start_date_tz'] = $piggyBankData['start_date']?->format('e');
$piggyBankData['target_date_tz'] = $piggyBankData['target_date']?->format('e');
$piggyBankData['account_id'] = null;
$piggyBankData['transaction_currency_id'] = $this->getCurrency($data)->id;
$piggyBankData['order'] = 131337;
try {
/** @var PiggyBank $piggyBank */
$piggyBank = PiggyBank::createQuietly($piggyBankData);
} catch (QueryException $e) {
Log::error(sprintf('Could not store piggy bank: %s', $e->getMessage()), $piggyBankData);
throw new FireflyException('400005: Could not store new piggy bank.', 0, $e);
}
$piggyBank = $this->setOrder($piggyBank, $data);
$this->linkToAccountIds($piggyBank, $data['accounts']);
$this->piggyBankRepository->updateNote($piggyBank, $data['notes']);
$objectGroupTitle = $data['object_group_title'] ?? '';
if ('' !== $objectGroupTitle) {
$objectGroup = $this->findOrCreateObjectGroup($objectGroupTitle);
if ($objectGroup instanceof ObjectGroup) {
$piggyBank->objectGroups()->sync([$objectGroup->id]);
}
}
// try also with ID
$objectGroupId = (int) ($data['object_group_id'] ?? 0);
if (0 !== $objectGroupId) {
$objectGroup = $this->findObjectGroupById($objectGroupId);
if ($objectGroup instanceof ObjectGroup) {
$piggyBank->objectGroups()->sync([$objectGroup->id]);
}
}
Log::debug('Touch piggy bank');
$piggyBank->encrypted = false;
$piggyBank->save();
$piggyBank->touch();
return $piggyBank;
}
private function getCurrency(array $data): TransactionCurrency
{
// currency:
$primaryCurrency = Amount::getPrimaryCurrency();
$currency = null;
if (array_key_exists('transaction_currency_code', $data)) {
$currency = $this->currencyRepository->findByCode((string) ($data['transaction_currency_code'] ?? ''));
}
if (array_key_exists('transaction_currency_id', $data)) {
$currency = $this->currencyRepository->find((int) ($data['transaction_currency_id'] ?? 0));
}
$currency ??= $primaryCurrency;
return $currency;
}
private function getMaxOrder(): int
{
return (int) $this->piggyBankRepository->getPiggyBanks()->max('order');
}
private function setOrder(PiggyBank $piggyBank, array $data): PiggyBank
{
$this->resetOrder();
$order = $this->getMaxOrder() + 1;
if (array_key_exists('order', $data)) {
$order = $data['order'];
}
$piggyBank->order = $order;
$piggyBank->saveQuietly();
return $piggyBank;
}
}

View File

@@ -38,6 +38,38 @@ class TagFactory
private User $user;
private UserGroup $userGroup;
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,
'user_group_id' => $this->userGroup->id,
'tag' => trim((string) $data['tag']),
'tag_mode' => 'nothing',
'date' => $data['date'],
'description' => $data['description'],
'latitude' => null,
'longitude' => null,
'zoomLevel' => null,
];
/** @var null|Tag $tag */
$tag = Tag::create($array);
if (!in_array(null, [$tag, $latitude, $longitude], true)) {
// create location object.
$location = new Location();
$location->latitude = $latitude;
$location->longitude = $longitude;
$location->zoom_level = $zoomLevel;
$location->locatable()->associate($tag);
$location->save();
}
return $tag;
}
public function findOrCreate(string $tag): ?Tag
{
$tag = trim($tag);
@@ -72,38 +104,6 @@ class TagFactory
return $newTag;
}
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,
'user_group_id' => $this->userGroup->id,
'tag' => trim((string) $data['tag']),
'tag_mode' => 'nothing',
'date' => $data['date'],
'description' => $data['description'],
'latitude' => null,
'longitude' => null,
'zoomLevel' => null,
];
/** @var null|Tag $tag */
$tag = Tag::create($array);
if (!in_array(null, [$tag, $latitude, $longitude], true)) {
// create location object.
$location = new Location();
$location->latitude = $latitude;
$location->longitude = $longitude;
$location->zoom_level = $zoomLevel;
$location->locatable()->associate($tag);
$location->save();
}
return $tag;
}
public function setUser(User $user): void
{
$this->user = $user;

View File

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

View File

@@ -150,6 +150,74 @@ class TransactionJournalFactory
return $collection;
}
public function setErrorOnHash(bool $errorOnHash): void
{
$this->errorOnHash = $errorOnHash;
if ($errorOnHash) {
Log::info('Will trigger duplication alert for this journal.');
}
}
/**
* Set the user.
*/
public function setUser(User $user): void
{
$this->user = $user;
$this->userGroup = $user->userGroup;
$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);
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
$this->currencyRepository->setUserGroup($userGroup);
$this->tagFactory->setUserGroup($userGroup);
$this->billRepository->setUserGroup($userGroup);
$this->budgetRepository->setUserGroup($userGroup);
$this->categoryRepository->setUserGroup($userGroup);
$this->piggyRepository->setUserGroup($userGroup);
$this->accountRepository->setUserGroup($userGroup);
}
protected function storeMeta(TransactionJournal $journal, array $data, string $field): void
{
$set = ['journal' => $journal, 'name' => $field, 'data' => (string) ($data[$field] ?? '')];
if (array_key_exists($field, $data) && $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:
*/
private function compareCurrencies(?TransactionCurrency $currency, ?TransactionCurrency $foreignCurrency): ?TransactionCurrency
{
Log::debug(sprintf('Now in compareCurrencies("%s", "%s")', $currency?->code, $foreignCurrency?->code));
if (!$currency instanceof TransactionCurrency) {
return null;
}
if ($foreignCurrency instanceof TransactionCurrency && $foreignCurrency->id === $currency->id) {
return null;
}
return $foreignCurrency;
}
/**
* TODO typeOverrule: the account validator may have another opinion on the transaction type. not sure what to do
* with this.
@@ -328,7 +396,7 @@ class TransactionJournalFactory
throw new FireflyException($e->getMessage(), 0, $e);
}
Log::debug(sprintf('Is part of a batch submission? %s', var_export($row['batch_submission'], true)));
// Log::debug(sprintf('Is part of a batch submission? %s', var_export($row['batch_submission'], true)));
$journal->save();
$this->storeBudget($journal, $row);
$this->storeCategory($journal, $row);
@@ -341,22 +409,6 @@ class TransactionJournalFactory
return $journal;
}
private function hashArray(NullArrayObject $row): string
{
unset($row['import_hash_v2'], $row['original_source']);
try {
$json = json_encode($row, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
Log::error(sprintf('Could not encode dataRow: %s', $e->getMessage()));
$json = microtime();
}
$hash = hash('sha256', $json);
Log::debug(sprintf('The hash is: %s', $hash), $row->getArrayCopy());
return $hash;
}
/**
* If this transaction already exists, throw an error.
*
@@ -391,6 +443,186 @@ class TransactionJournalFactory
}
}
/**
* Force the deletion of an entire set of transaction journals and their meta object in case of
* an error creating a group.
*/
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);
}
}
private function forceTrDelete(Transaction $transaction): void
{
$transaction->delete();
}
private function getCurrency(?TransactionCurrency $currency, Account $account): TransactionCurrency
{
Log::debug(sprintf('Now in getCurrency(#%d, "%s")', $currency?->id, $account->name));
/** @var null|TransactionCurrency $preference */
$preference = $this->accountRepository->getAccountCurrency($account);
if (null === $preference && !$currency instanceof TransactionCurrency) {
// return user's default:
return Amount::getPrimaryCurrencyByUserGroup($this->user->userGroup);
}
$result = $preference ?? $currency;
Log::debug(sprintf('Currency is now #%d (%s) because of account #%d (%s)', $result->id, $result->code, $account->id, $account->name));
return $result;
}
/**
* @throws FireflyException
*/
private function getCurrencyByAccount(string $type, ?TransactionCurrency $currency, Account $source, Account $destination): TransactionCurrency
{
Log::debug('Now in getCurrencyByAccount()');
/*
* Deze functie moet bij een transactie van liability naar asset wel degelijk de currency
* van de liability teruggeven en niet die van de destination. Fix voor #10265
*/
if ($this->isBetweenAssetAndLiability($source, $destination) && TransactionTypeEnum::DEPOSIT->value === $type) {
return $this->getCurrency($currency, $source);
}
return match ($type) {
default => $this->getCurrency($currency, $source),
TransactionTypeEnum::DEPOSIT->value => $this->getCurrency($currency, $destination)
};
}
private function getDescription(string $description): string
{
$description = '' === $description ? '(empty description)' : $description;
return substr($description, 0, 1024);
}
/**
* @throws FireflyException
*/
private function getForeignByAccount(string $type, ?TransactionCurrency $foreignCurrency, Account $destination): ?TransactionCurrency
{
Log::debug(sprintf('Now in getForeignByAccount("%s", #%d, "%s")', $type, $foreignCurrency?->id, $destination->name));
if (TransactionTypeEnum::TRANSFER->value === $type) {
return $this->getCurrency($foreignCurrency, $destination);
}
return $foreignCurrency;
}
private function hashArray(NullArrayObject $row): string
{
unset($row['import_hash_v2'], $row['original_source']);
try {
$json = json_encode($row, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
Log::error(sprintf('Could not encode dataRow: %s', $e->getMessage()));
$json = microtime();
}
$hash = hash('sha256', $json);
Log::debug(sprintf('The hash is: %s', $hash), $row->getArrayCopy());
return $hash;
}
private function isBetweenAssetAndLiability(Account $source, Account $destination): bool
{
$sourceTypes = [AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value];
// source is liability, destination is asset
if (in_array($source->accountType->type, $sourceTypes, true) && AccountTypeEnum::ASSET->value === $destination->accountType->type) {
Log::debug('Source is a liability account, destination is an asset account, return TRUE.');
return true;
}
// source is asset, destination is liability
if (in_array($destination->accountType->type, $sourceTypes, true) && AccountTypeEnum::ASSET->value === $source->accountType->type) {
Log::debug('Destination is a liability account, source is an asset account, return TRUE.');
return true;
}
Log::debug('Not between asset and liability, return FALSE');
return false;
}
private function reconciliationSanityCheck(?Account $sourceAccount, ?Account $destinationAccount): array
{
Log::debug(sprintf('Now in %s', __METHOD__));
if ($sourceAccount instanceof Account && $destinationAccount instanceof Account) {
Log::debug('Both accounts exist, simply return them.');
return [$sourceAccount, $destinationAccount];
}
if (!$destinationAccount instanceof Account) {
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 (!$sourceAccount instanceof Account) { // @phpstan-ignore-line
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'); // @phpstan-ignore-line
return [$sourceAccount, $destinationAccount];
}
private function storeLocation(TransactionJournal $journal, NullArrayObject $data): void
{
if (!in_array(null, [$data['longitude'], $data['latitude'], $data['zoom_level']], true)) {
$location = new Location();
$location->longitude = $data['longitude'];
$location->latitude = $data['latitude'];
$location->zoom_level = $data['zoom_level'];
$location->locatable()->associate($journal);
$location->save();
}
}
private function storeMetaFields(TransactionJournal $journal, NullArrayObject $transaction): void
{
foreach ($this->fields as $field) {
$this->storeMeta($journal, $transaction->getArrayCopy(), $field);
}
}
/**
* Link a piggy bank to this journal.
*/
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 ($piggyBank instanceof PiggyBank) {
$this->piggyEventFactory->create($journal, $piggyBank);
Log::debug('Create piggy event.');
return;
}
Log::debug('Create no piggy event');
}
/**
* @throws FireflyException
*/
@@ -430,236 +662,4 @@ class TransactionJournalFactory
throw new FireflyException(sprintf('Destination: %s', $this->accountValidator->destError));
}
}
/**
* Set the user.
*/
public function setUser(User $user): void
{
$this->user = $user;
$this->userGroup = $user->userGroup;
$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);
}
private function reconciliationSanityCheck(?Account $sourceAccount, ?Account $destinationAccount): array
{
Log::debug(sprintf('Now in %s', __METHOD__));
if ($sourceAccount instanceof Account && $destinationAccount instanceof Account) {
Log::debug('Both accounts exist, simply return them.');
return [$sourceAccount, $destinationAccount];
}
if (!$destinationAccount instanceof Account) {
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 (!$sourceAccount instanceof Account) { // @phpstan-ignore-line
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'); // @phpstan-ignore-line
return [$sourceAccount, $destinationAccount];
}
/**
* @throws FireflyException
*/
private function getCurrencyByAccount(string $type, ?TransactionCurrency $currency, Account $source, Account $destination): TransactionCurrency
{
Log::debug('Now in getCurrencyByAccount()');
/*
* Deze functie moet bij een transactie van liability naar asset wel degelijk de currency
* van de liability teruggeven en niet die van de destination. Fix voor #10265
*/
if ($this->isBetweenAssetAndLiability($source, $destination) && TransactionTypeEnum::DEPOSIT->value === $type) {
return $this->getCurrency($currency, $source);
}
return match ($type) {
default => $this->getCurrency($currency, $source),
TransactionTypeEnum::DEPOSIT->value => $this->getCurrency($currency, $destination)
};
}
private function getCurrency(?TransactionCurrency $currency, Account $account): TransactionCurrency
{
Log::debug(sprintf('Now in getCurrency(#%d, "%s")', $currency?->id, $account->name));
/** @var null|TransactionCurrency $preference */
$preference = $this->accountRepository->getAccountCurrency($account);
if (null === $preference && !$currency instanceof TransactionCurrency) {
// return user's default:
return Amount::getPrimaryCurrencyByUserGroup($this->user->userGroup);
}
$result = $preference ?? $currency;
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:
*/
private function compareCurrencies(?TransactionCurrency $currency, ?TransactionCurrency $foreignCurrency): ?TransactionCurrency
{
Log::debug(sprintf('Now in compareCurrencies("%s", "%s")', $currency?->code, $foreignCurrency?->code));
if (!$currency instanceof TransactionCurrency) {
return null;
}
if ($foreignCurrency instanceof TransactionCurrency && $foreignCurrency->id === $currency->id) {
return null;
}
return $foreignCurrency;
}
/**
* @throws FireflyException
*/
private function getForeignByAccount(string $type, ?TransactionCurrency $foreignCurrency, Account $destination): ?TransactionCurrency
{
Log::debug(sprintf('Now in getForeignByAccount("%s", #%d, "%s")', $type, $foreignCurrency?->id, $destination->name));
if (TransactionTypeEnum::TRANSFER->value === $type) {
return $this->getCurrency($foreignCurrency, $destination);
}
return $foreignCurrency;
}
private function getDescription(string $description): string
{
$description = '' === $description ? '(empty description)' : $description;
return substr($description, 0, 1024);
}
/**
* Force the deletion of an entire set of transaction journals and their meta object in case of
* an error creating a group.
*/
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);
}
}
private function forceTrDelete(Transaction $transaction): void
{
$transaction->delete();
}
/**
* Link a piggy bank to this journal.
*/
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 ($piggyBank instanceof PiggyBank) {
$this->piggyEventFactory->create($journal, $piggyBank);
Log::debug('Create piggy event.');
return;
}
Log::debug('Create no piggy event');
}
private function storeMetaFields(TransactionJournal $journal, NullArrayObject $transaction): void
{
foreach ($this->fields as $field) {
$this->storeMeta($journal, $transaction->getArrayCopy(), $field);
}
}
protected function storeMeta(TransactionJournal $journal, array $data, string $field): void
{
$set = ['journal' => $journal, 'name' => $field, 'data' => (string) ($data[$field] ?? '')];
if (array_key_exists($field, $data) && $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);
}
private function storeLocation(TransactionJournal $journal, NullArrayObject $data): void
{
if (!in_array(null, [$data['longitude'], $data['latitude'], $data['zoom_level']], true)) {
$location = new Location();
$location->longitude = $data['longitude'];
$location->latitude = $data['latitude'];
$location->zoom_level = $data['zoom_level'];
$location->locatable()->associate($journal);
$location->save();
}
}
public function setErrorOnHash(bool $errorOnHash): void
{
$this->errorOnHash = $errorOnHash;
if ($errorOnHash) {
Log::info('Will trigger duplication alert for this journal.');
}
}
public function setUserGroup(UserGroup $userGroup): void
{
$this->userGroup = $userGroup;
$this->currencyRepository->setUserGroup($userGroup);
$this->tagFactory->setUserGroup($userGroup);
$this->billRepository->setUserGroup($userGroup);
$this->budgetRepository->setUserGroup($userGroup);
$this->categoryRepository->setUserGroup($userGroup);
$this->piggyRepository->setUserGroup($userGroup);
$this->accountRepository->setUserGroup($userGroup);
}
private function isBetweenAssetAndLiability(Account $source, Account $destination): bool
{
$sourceTypes = [AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value];
// source is liability, destination is asset
if (in_array($source->accountType->type, $sourceTypes, true) && AccountTypeEnum::ASSET->value === $destination->accountType->type) {
Log::debug('Source is a liability account, destination is an asset account, return TRUE.');
return true;
}
// source is asset, destination is liability
if (in_array($destination->accountType->type, $sourceTypes, true) && AccountTypeEnum::ASSET->value === $source->accountType->type) {
Log::debug('Destination is a liability account, source is an asset account, return TRUE.');
return true;
}
Log::debug('Not between asset and liability, return FALSE');
return false;
}
}

View File

@@ -75,14 +75,6 @@ class MonthReportGenerator implements ReportGeneratorInterface
return $result;
}
/**
* Return the preferred period.
*/
protected function preferredPeriod(): string
{
return 'day';
}
/**
* Set accounts.
*/
@@ -146,4 +138,12 @@ class MonthReportGenerator implements ReportGeneratorInterface
{
return $this;
}
/**
* Return the preferred period.
*/
protected function preferredPeriod(): string
{
return 'day';
}
}

View File

@@ -73,6 +73,26 @@ class MonthReportGenerator implements ReportGeneratorInterface
return $result;
}
/**
* Set the involved accounts.
*/
public function setAccounts(Collection $accounts): ReportGeneratorInterface
{
$this->accounts = $accounts;
return $this;
}
/**
* Set the involved budgets.
*/
public function setBudgets(Collection $budgets): ReportGeneratorInterface
{
$this->budgets = $budgets;
return $this;
}
/**
* Unused category setter.
*/
@@ -144,24 +164,4 @@ class MonthReportGenerator implements ReportGeneratorInterface
return $journals;
}
/**
* Set the involved budgets.
*/
public function setBudgets(Collection $budgets): ReportGeneratorInterface
{
$this->budgets = $budgets;
return $this;
}
/**
* Set the involved accounts.
*/
public function setAccounts(Collection $accounts): ReportGeneratorInterface
{
$this->accounts = $accounts;
return $this;
}
}

View File

@@ -74,6 +74,16 @@ class MonthReportGenerator implements ReportGeneratorInterface
}
}
/**
* Set the involved accounts.
*/
public function setAccounts(Collection $accounts): ReportGeneratorInterface
{
$this->accounts = $accounts;
return $this;
}
/**
* Empty budget setter.
*/
@@ -82,6 +92,16 @@ class MonthReportGenerator implements ReportGeneratorInterface
return $this;
}
/**
* Set the categories involved in this report.
*/
public function setCategories(Collection $categories): ReportGeneratorInterface
{
$this->categories = $categories;
return $this;
}
/**
* Set the end date for this report.
*/
@@ -145,26 +165,6 @@ class MonthReportGenerator implements ReportGeneratorInterface
return $transactions;
}
/**
* Set the categories involved in this report.
*/
public function setCategories(Collection $categories): ReportGeneratorInterface
{
$this->categories = $categories;
return $this;
}
/**
* Set the involved accounts.
*/
public function setAccounts(Collection $accounts): ReportGeneratorInterface
{
$this->accounts = $accounts;
return $this;
}
/**
* Get the income for this report.
*/

View File

@@ -84,43 +84,44 @@ class StandardMessageGenerator implements MessageGeneratorInterface
$this->run();
}
private function getWebhooks(): Collection
public function getVersion(): int
{
return $this->user
->webhooks()
->leftJoin('webhook_webhook_trigger', 'webhook_webhook_trigger.webhook_id', 'webhooks.id')
->leftJoin('webhook_triggers', 'webhook_webhook_trigger.webhook_trigger_id', 'webhook_triggers.id')
->where('active', true)
->whereIn('webhook_triggers.title', [$this->trigger->name, WebhookTrigger::ANY->name])
->get(['webhooks.*'])
;
return $this->version;
}
/**
* @throws FireflyException
*/
private function run(): void
public function setObjects(Collection $objects): void
{
Log::debug('Now in StandardMessageGenerator::run');
/** @var Webhook $webhook */
foreach ($this->webhooks as $webhook) {
$this->runWebhook($webhook);
}
Log::debug('Done with StandardMessageGenerator::run');
$this->objects = $objects;
}
/**
* @throws FireflyException
*/
private function runWebhook(Webhook $webhook): void
public function setTrigger(WebhookTrigger $trigger): void
{
Log::debug(sprintf('Now in runWebhook(#%d)', $webhook->id));
$this->trigger = $trigger;
}
/** @var Model $object */
foreach ($this->objects as $object) {
$this->generateMessage($webhook, $object);
public function setUser(User $user): void
{
$this->user = $user;
}
public function setWebhooks(Collection $webhooks): void
{
$this->webhooks = $webhooks;
}
private function collectAccounts(TransactionGroup $transactionGroup): Collection
{
$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();
}
/**
@@ -258,46 +259,6 @@ class StandardMessageGenerator implements MessageGeneratorInterface
$factory->create($webhook, $basicMessage);
}
public function getVersion(): int
{
return $this->version;
}
private function collectAccounts(TransactionGroup $transactionGroup): Collection
{
$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();
}
public function setObjects(Collection $objects): void
{
$this->objects = $objects;
}
public function setTrigger(WebhookTrigger $trigger): void
{
$this->trigger = $trigger;
}
public function setUser(User $user): void
{
$this->user = $user;
}
public function setWebhooks(Collection $webhooks): void
{
$this->webhooks = $webhooks;
}
private function getRelevantResponse(WebhookResponseModel $response, string $class): string
{
// return none if none.
@@ -343,4 +304,43 @@ class StandardMessageGenerator implements MessageGeneratorInterface
return array_unique($return);
}
private function getWebhooks(): Collection
{
return $this->user
->webhooks()
->leftJoin('webhook_webhook_trigger', 'webhook_webhook_trigger.webhook_id', 'webhooks.id')
->leftJoin('webhook_triggers', 'webhook_webhook_trigger.webhook_trigger_id', 'webhook_triggers.id')
->where('active', true)
->whereIn('webhook_triggers.title', [$this->trigger->name, WebhookTrigger::ANY->name])
->get(['webhooks.*'])
;
}
/**
* @throws FireflyException
*/
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');
}
/**
* @throws FireflyException
*/
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);
}
}
}

View File

@@ -37,7 +37,7 @@ class ConvertsAmountToPrimaryAmount
if (!Amount::convertToPrimary($params->user)) {
Log::debug(sprintf(
'User does not want to do conversion, no need to convert %s and store it in field %s for %s #%d.',
'User does not want to do conversion, no need to convert "%s" and store it in field "%s" for %s #%d.',
$params->amountField,
$params->primaryAmountField,
get_class($params->model),
@@ -84,9 +84,9 @@ class ConvertsAmountToPrimaryAmount
return;
}
$converter = new ExchangeRateConverter();
$newAmount = $converter->convert($params->originalCurrency, $primaryCurrency, now(), $amount);
$converter->setUserGroup($params->user->userGroup);
$converter->setIgnoreSettings(true);
$newAmount = $converter->convert($params->originalCurrency, $primaryCurrency, now(), $amount);
$params->model->{$primaryAmountField} = $newAmount;
$params->model->saveQuietly();
Log::debug(sprintf(

View File

@@ -25,9 +25,7 @@ namespace FireflyIII\Handlers\Observer;
use FireflyIII\Handlers\ExchangeRate\ConversionParameters;
use FireflyIII\Handlers\ExchangeRate\ConvertsAmountToPrimaryAmount;
use FireflyIII\Models\Attachment;
use FireflyIII\Models\Bill;
use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface;
/**
* Class BillObserver
@@ -39,18 +37,6 @@ class BillObserver
$this->updatePrimaryCurrencyAmount($bill);
}
public function deleting(Bill $bill): void
{
$repository = app(AttachmentRepositoryInterface::class);
$repository->setUser($bill->user);
/** @var Attachment $attachment */
foreach ($bill->attachments()->get() as $attachment) {
$repository->destroy($attachment);
}
$bill->notes()->delete();
}
public function updated(Bill $bill): void
{
// Log::debug('Observe "updated" of a bill.');

View File

@@ -24,55 +24,23 @@ declare(strict_types=1);
namespace FireflyIII\Handlers\Observer;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\Model\Webhook\WebhookMessagesRequestSending;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Handlers\ExchangeRate\ConversionParameters;
use FireflyIII\Handlers\ExchangeRate\ConvertsAmountToPrimaryAmount;
use FireflyIII\Models\BudgetLimit;
use FireflyIII\Support\Observers\RecalculatesAvailableBudgetsTrait;
use FireflyIII\Support\Singleton\PreferencesSingleton;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class BudgetLimitObserver
{
use RecalculatesAvailableBudgetsTrait;
public function created(BudgetLimit $budgetLimit): void
{
Log::debug('Observe "created" of a budget limit.');
$this->updatePrimaryCurrencyAmount($budgetLimit);
$this->updateAvailableBudget($budgetLimit);
$this->sendWebhookMessages('fire_webhooks_bl_store', WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT, $budgetLimit);
}
public function updated(BudgetLimit $budgetLimit): void
{
Log::debug('Observe "updated" of a budget limit.');
$this->updatePrimaryCurrencyAmount($budgetLimit);
$this->updateAvailableBudget($budgetLimit);
$this->sendWebhookMessages('fire_webhooks_bl_update', WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT, $budgetLimit);
}
private function sendWebhookMessages(string $key, WebhookTrigger $trigger, BudgetLimit $budgetLimit): void
{
// this is a lame trick to communicate with the observer.
$singleton = PreferencesSingleton::getInstance();
if (true === $singleton->getPreference($key)) {
$user = $budgetLimit->budget->user;
/** @var MessageGeneratorInterface $engine */
$engine = app(MessageGeneratorInterface::class);
$engine->setUser($user);
$engine->setObjects(new Collection()->push($budgetLimit));
$engine->setTrigger($trigger);
$engine->generateMessages();
Log::debug(sprintf('send event WebhookMessagesRequestSending from %s', __METHOD__));
event(new WebhookMessagesRequestSending());
}
}
private function updatePrimaryCurrencyAmount(BudgetLimit $budgetLimit): void

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