diff --git a/.ci/php-cs-fixer/composer.lock b/.ci/php-cs-fixer/composer.lock
index a7ad9afcb3..f397529f05 100644
--- a/.ci/php-cs-fixer/composer.lock
+++ b/.ci/php-cs-fixer/composer.lock
@@ -402,16 +402,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
- "version": "v3.87.2",
+ "version": "v3.88.2",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
- "reference": "da5f0a7858c79b56fc0b8c36d3efcfe5f37f0992"
+ "reference": "a8d15584bafb0f0d9d938827840060fd4a3ebc99"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/da5f0a7858c79b56fc0b8c36d3efcfe5f37f0992",
- "reference": "da5f0a7858c79b56fc0b8c36d3efcfe5f37f0992",
+ "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/a8d15584bafb0f0d9d938827840060fd4a3ebc99",
+ "reference": "a8d15584bafb0f0d9d938827840060fd4a3ebc99",
"shasum": ""
},
"require": {
@@ -438,12 +438,13 @@
"symfony/polyfill-mbstring": "^1.33",
"symfony/polyfill-php80": "^1.33",
"symfony/polyfill-php81": "^1.33",
+ "symfony/polyfill-php84": "^1.33",
"symfony/process": "^5.4.47 || ^6.4.24 || ^7.2",
"symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0"
},
"require-dev": {
"facile-it/paraunit": "^1.3.1 || ^2.7",
- "infection/infection": "^0.29.14",
+ "infection/infection": "^0.31.0",
"justinrainbow/json-schema": "^6.5",
"keradus/cli-executor": "^2.2",
"mikey179/vfsstream": "^1.6.12",
@@ -451,7 +452,6 @@
"php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6",
"php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6",
"phpunit/phpunit": "^9.6.25 || ^10.5.53 || ^11.5.34",
- "symfony/polyfill-php84": "^1.33",
"symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2",
"symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2"
},
@@ -494,7 +494,7 @@
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
- "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.87.2"
+ "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.88.2"
},
"funding": [
{
@@ -502,7 +502,7 @@
"type": "github"
}
],
- "time": "2025-09-10T09:51:40+00:00"
+ "time": "2025-09-27T00:24:15+00:00"
},
{
"name": "psr/container",
@@ -2283,6 +2283,86 @@
],
"time": "2024-09-09T11:45:10+00:00"
},
+ {
+ "name": "symfony/polyfill-php84",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php84.git",
+ "reference": "d8ced4d875142b6a7426000426b8abc631d6b191"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191",
+ "reference": "d8ced4d875142b6a7426000426b8abc631d6b191",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php84\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-06-24T13:30:11+00:00"
+ },
{
"name": "symfony/process",
"version": "v7.3.3",
diff --git a/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php b/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php
index de08a181b5..25fc1f145e 100644
--- a/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php
+++ b/app/Api/V1/Controllers/Autocomplete/PiggyBankController.php
@@ -28,8 +28,10 @@ use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\Autocomplete\AutocompleteRequest;
use FireflyIII\Enums\UserRoleEnum;
use FireflyIII\Models\PiggyBank;
+use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
+use FireflyIII\Support\Facades\Amount;
use Illuminate\Http\JsonResponse;
/**
@@ -96,6 +98,7 @@ class PiggyBankController extends Controller
/** @var PiggyBank $piggy */
foreach ($piggies as $piggy) {
+ /** @var TransactionCurrency $currency */
$currency = $piggy->transactionCurrency;
$currentAmount = $this->piggyRepository->getCurrentAmount($piggy);
$objectGroup = $piggy->objectGroups()->first();
@@ -105,8 +108,8 @@ class PiggyBankController extends Controller
'name_with_balance' => sprintf(
'%s (%s / %s)',
$piggy->name,
- app('amount')->formatAnything($currency, $currentAmount, false),
- app('amount')->formatAnything($currency, $piggy->target_amount, false),
+ Amount::formatAnything($currency, $currentAmount, false),
+ Amount::formatAnything($currency, $piggy->target_amount, false),
),
'currency_id' => (string) $currency->id,
'currency_name' => $currency->name,
diff --git a/app/Api/V1/Controllers/Models/Budget/StoreController.php b/app/Api/V1/Controllers/Models/Budget/StoreController.php
index b6d9e85e6c..66a8f36153 100644
--- a/app/Api/V1/Controllers/Models/Budget/StoreController.php
+++ b/app/Api/V1/Controllers/Models/Budget/StoreController.php
@@ -67,7 +67,9 @@ class StoreController extends Controller
*/
public function store(StoreRequest $request): JsonResponse
{
- $budget = $this->repository->store($request->getAll());
+ $data = $request->getAll();
+ $data['fire_webhooks'] ??= true;
+ $budget = $this->repository->store($data);
$budget->refresh();
$manager = $this->getManager();
diff --git a/app/Api/V1/Controllers/Models/Budget/UpdateController.php b/app/Api/V1/Controllers/Models/Budget/UpdateController.php
index b6524ba738..54bddb72a6 100644
--- a/app/Api/V1/Controllers/Models/Budget/UpdateController.php
+++ b/app/Api/V1/Controllers/Models/Budget/UpdateController.php
@@ -57,15 +57,10 @@ class UpdateController extends Controller
);
}
- /**
- * This endpoint is documented at:
- * https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/budgets/updateBudget
- *
- * Update a budget.
- */
public function update(UpdateRequest $request, Budget $budget): JsonResponse
{
$data = $request->getAll();
+ $data['fire_webhooks'] ??= true;
$budget = $this->repository->update($budget, $data);
$manager = $this->getManager();
diff --git a/app/Api/V1/Controllers/Models/BudgetLimit/StoreController.php b/app/Api/V1/Controllers/Models/BudgetLimit/StoreController.php
index c3b88d69f2..3b16016efa 100644
--- a/app/Api/V1/Controllers/Models/BudgetLimit/StoreController.php
+++ b/app/Api/V1/Controllers/Models/BudgetLimit/StoreController.php
@@ -70,6 +70,7 @@ class StoreController extends Controller
$data = $request->getAll();
$data['start_date'] = $data['start'];
$data['end_date'] = $data['end'];
+ $data['fire_webhooks'] ??= true;
$data['budget_id'] = $budget->id;
$budgetLimit = $this->blRepository->store($data);
diff --git a/app/Api/V1/Controllers/Models/BudgetLimit/UpdateController.php b/app/Api/V1/Controllers/Models/BudgetLimit/UpdateController.php
index cd17067b5c..3517062e2b 100644
--- a/app/Api/V1/Controllers/Models/BudgetLimit/UpdateController.php
+++ b/app/Api/V1/Controllers/Models/BudgetLimit/UpdateController.php
@@ -77,6 +77,7 @@ class UpdateController extends Controller
throw new FireflyException('20028: The budget limit does not belong to the budget.');
}
$data = $request->getAll();
+ $data['fire_webhooks'] ??= true;
$data['budget_id'] = $budget->id;
$budgetLimit = $this->blRepository->update($budgetLimit, $data);
$manager = $this->getManager();
diff --git a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php
index 265a72b616..733517a506 100644
--- a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php
+++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php
@@ -72,7 +72,7 @@ class DestroyController extends Controller
public function destroySingleByDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
{
$exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date);
- if (null !== $exchangeRate) {
+ if ($exchangeRate instanceof CurrencyExchangeRate) {
$this->repository->deleteRate($exchangeRate);
}
diff --git a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php
index 3b2f8c6e4d..22a9756ab7 100644
--- a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php
+++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php
@@ -95,7 +95,7 @@ class ShowController extends Controller
$transformer->setParameters($this->parameters);
$exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date);
- if (null === $exchangeRate) {
+ if (!$exchangeRate instanceof CurrencyExchangeRate) {
throw new NotFoundHttpException();
}
diff --git a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/UpdateController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/UpdateController.php
index 8844407f7d..8326f44c52 100644
--- a/app/Api/V1/Controllers/Models/CurrencyExchangeRate/UpdateController.php
+++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/UpdateController.php
@@ -74,7 +74,7 @@ class UpdateController extends Controller
public function updateByDate(UpdateRequest $request, TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
{
$exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date);
- if (null === $exchangeRate) {
+ if (!$exchangeRate instanceof CurrencyExchangeRate) {
throw new NotFoundHttpException();
}
$date = $request->getDate();
diff --git a/app/Api/V1/Controllers/Webhook/ShowController.php b/app/Api/V1/Controllers/Webhook/ShowController.php
index c0a27d5fce..03249281c3 100644
--- a/app/Api/V1/Controllers/Webhook/ShowController.php
+++ b/app/Api/V1/Controllers/Webhook/ShowController.php
@@ -158,18 +158,23 @@ class ShowController extends Controller
Log::debug(sprintf('Now in triggerTransaction(%d, %d)', $webhook->id, $group->id));
Log::channel('audit')->info(sprintf('User triggers webhook #%d on transaction group #%d.', $webhook->id, $group->id));
- /** @var MessageGeneratorInterface $engine */
- $engine = app(MessageGeneratorInterface::class);
- $engine->setUser(auth()->user());
- // tell the generator which trigger it should look for
- $engine->setTrigger(WebhookTrigger::tryFrom($webhook->trigger));
- // tell the generator which objects to process
- $engine->setObjects(new Collection()->push($group));
- // set the webhook to trigger
- $engine->setWebhooks(new Collection()->push($webhook));
- // tell the generator to generate the messages
- $engine->generateMessages();
+ /** @var \FireflyIII\Models\WebhookTrigger $trigger */
+ foreach ($webhook->webhookTriggers as $trigger) {
+ /** @var MessageGeneratorInterface $engine */
+ $engine = app(MessageGeneratorInterface::class);
+ $engine->setUser(auth()->user());
+
+ // tell the generator which trigger it should look for
+ $engine->setTrigger(WebhookTrigger::tryFrom((int)$trigger->key));
+ // tell the generator which objects to process
+ $engine->setObjects(new Collection()->push($group));
+ // set the webhook to trigger
+ $engine->setWebhooks(new Collection()->push($webhook));
+ // tell the generator to generate the messages
+ $engine->generateMessages();
+ }
+
// trigger event to send them:
Log::debug('send event RequestedSendWebhookMessages from ShowController::triggerTransaction()');
diff --git a/app/Api/V1/Requests/Models/Budget/StoreRequest.php b/app/Api/V1/Requests/Models/Budget/StoreRequest.php
index 9a0118406d..fd36873cbb 100644
--- a/app/Api/V1/Requests/Models/Budget/StoreRequest.php
+++ b/app/Api/V1/Requests/Models/Budget/StoreRequest.php
@@ -48,17 +48,20 @@ class StoreRequest extends FormRequest
public function getAll(): array
{
$fields = [
- 'name' => ['name', 'convertString'],
- 'active' => ['active', 'boolean'],
- 'order' => ['active', 'convertInteger'],
- 'notes' => ['notes', 'convertString'],
+ 'name' => ['name', 'convertString'],
+ 'active' => ['active', 'boolean'],
+ 'order' => ['active', 'convertInteger'],
+ 'notes' => ['notes', 'convertString'],
// auto budget currency:
- 'currency_id' => ['auto_budget_currency_id', 'convertInteger'],
- 'currency_code' => ['auto_budget_currency_code', 'convertString'],
- 'auto_budget_type' => ['auto_budget_type', 'convertString'],
- 'auto_budget_amount' => ['auto_budget_amount', 'convertString'],
- 'auto_budget_period' => ['auto_budget_period', 'convertString'],
+ 'currency_id' => ['auto_budget_currency_id', 'convertInteger'],
+ 'currency_code' => ['auto_budget_currency_code', 'convertString'],
+ 'auto_budget_type' => ['auto_budget_type', 'convertString'],
+ 'auto_budget_amount' => ['auto_budget_amount', 'convertString'],
+ 'auto_budget_period' => ['auto_budget_period', 'convertString'],
+
+ // webhooks
+ 'fire_webhooks' => ['fire_webhooks', 'boolean'],
];
return $this->getAllData($fields);
@@ -70,15 +73,18 @@ class StoreRequest extends FormRequest
public function rules(): array
{
return [
- 'name' => 'required|min:1|max:255|uniqueObjectForUser:budgets,name',
- 'active' => [new IsBoolean()],
- 'currency_id' => 'exists:transaction_currencies,id',
- 'currency_code' => 'exists:transaction_currencies,code',
- 'notes' => 'nullable|min:1|max:32768',
+ 'name' => 'required|min:1|max:255|uniqueObjectForUser:budgets,name',
+ 'active' => [new IsBoolean()],
+ 'currency_id' => 'exists:transaction_currencies,id',
+ 'currency_code' => 'exists:transaction_currencies,code',
+ 'notes' => 'nullable|min:1|max:32768',
// auto budget info
- 'auto_budget_type' => 'in:reset,rollover,adjusted,none',
- 'auto_budget_amount' => ['required_if:auto_budget_type,reset', 'required_if:auto_budget_type,rollover', 'required_if:auto_budget_type,adjusted', new IsValidPositiveAmount()],
- 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly|required_if:auto_budget_type,reset|required_if:auto_budget_type,rollover|required_if:auto_budget_type,adjusted',
+ 'auto_budget_type' => 'in:reset,rollover,adjusted,none',
+ 'auto_budget_amount' => ['required_if:auto_budget_type,reset', 'required_if:auto_budget_type,rollover', 'required_if:auto_budget_type,adjusted', new IsValidPositiveAmount()],
+ 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly|required_if:auto_budget_type,reset|required_if:auto_budget_type,rollover|required_if:auto_budget_type,adjusted',
+
+ // webhooks
+ 'fire_webhooks' => [new IsBoolean()],
];
}
diff --git a/app/Api/V1/Requests/Models/Budget/UpdateRequest.php b/app/Api/V1/Requests/Models/Budget/UpdateRequest.php
index 6eb0dc7acc..870cc3294f 100644
--- a/app/Api/V1/Requests/Models/Budget/UpdateRequest.php
+++ b/app/Api/V1/Requests/Models/Budget/UpdateRequest.php
@@ -50,15 +50,18 @@ class UpdateRequest extends FormRequest
{
// this is the way:
$fields = [
- 'name' => ['name', 'convertString'],
- 'active' => ['active', 'boolean'],
- 'order' => ['order', 'convertInteger'],
- 'notes' => ['notes', 'convertString'],
- 'currency_id' => ['auto_budget_currency_id', 'convertInteger'],
- 'currency_code' => ['auto_budget_currency_code', 'convertString'],
- 'auto_budget_type' => ['auto_budget_type', 'convertString'],
- 'auto_budget_amount' => ['auto_budget_amount', 'convertString'],
- 'auto_budget_period' => ['auto_budget_period', 'convertString'],
+ 'name' => ['name', 'convertString'],
+ 'active' => ['active', 'boolean'],
+ 'order' => ['order', 'convertInteger'],
+ 'notes' => ['notes', 'convertString'],
+ 'currency_id' => ['auto_budget_currency_id', 'convertInteger'],
+ 'currency_code' => ['auto_budget_currency_code', 'convertString'],
+ 'auto_budget_type' => ['auto_budget_type', 'convertString'],
+ 'auto_budget_amount' => ['auto_budget_amount', 'convertString'],
+ 'auto_budget_period' => ['auto_budget_period', 'convertString'],
+
+ // webhooks
+ 'fire_webhooks' => ['fire_webhooks', 'boolean'],
];
$allData = $this->getAllData($fields);
if (array_key_exists('auto_budget_type', $allData)) {
@@ -83,14 +86,17 @@ class UpdateRequest extends FormRequest
$budget = $this->route()->parameter('budget');
return [
- 'name' => sprintf('min:1|max:100|uniqueObjectForUser:budgets,name,%d', $budget->id),
- 'active' => [new IsBoolean()],
- 'notes' => 'nullable|min:1|max:32768',
- 'auto_budget_type' => 'in:reset,rollover,adjusted,none',
- 'auto_budget_currency_id' => 'exists:transaction_currencies,id',
- 'auto_budget_currency_code' => 'exists:transaction_currencies,code',
- 'auto_budget_amount' => ['nullable', new IsValidPositiveAmount()],
- 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly',
+ 'name' => sprintf('min:1|max:100|uniqueObjectForUser:budgets,name,%d', $budget->id),
+ 'active' => [new IsBoolean()],
+ 'notes' => 'nullable|min:1|max:32768',
+ 'auto_budget_type' => 'in:reset,rollover,adjusted,none',
+ 'auto_budget_currency_id' => 'exists:transaction_currencies,id',
+ 'auto_budget_currency_code' => 'exists:transaction_currencies,code',
+ 'auto_budget_amount' => ['nullable', new IsValidPositiveAmount()],
+ 'auto_budget_period' => 'in:daily,weekly,monthly,quarterly,half_year,yearly',
+
+ // webhooks
+ 'fire_webhooks' => [new IsBoolean()],
];
}
diff --git a/app/Api/V1/Requests/Models/BudgetLimit/StoreRequest.php b/app/Api/V1/Requests/Models/BudgetLimit/StoreRequest.php
index 48c77cf2a2..1a705223de 100644
--- a/app/Api/V1/Requests/Models/BudgetLimit/StoreRequest.php
+++ b/app/Api/V1/Requests/Models/BudgetLimit/StoreRequest.php
@@ -24,10 +24,16 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\BudgetLimit;
+use Carbon\Carbon;
+use FireflyIII\Factory\TransactionCurrencyFactory;
+use FireflyIII\Rules\IsBoolean;
use FireflyIII\Rules\IsValidPositiveAmount;
+use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Foundation\Http\FormRequest;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Validation\Validator;
/**
* Class StoreRequest
@@ -49,6 +55,9 @@ class StoreRequest extends FormRequest
'currency_id' => $this->convertInteger('currency_id'),
'currency_code' => $this->convertString('currency_code'),
'notes' => $this->stringWithNewlines('notes'),
+
+ // for webhooks:
+ 'fire_webhooks' => $this->boolean('fire_webhooks', true),
];
}
@@ -58,12 +67,59 @@ class StoreRequest extends FormRequest
public function rules(): array
{
return [
- 'start' => 'required|before:end|date',
- 'end' => 'required|after:start|date',
- 'amount' => ['required', new IsValidPositiveAmount()],
- 'currency_id' => 'numeric|exists:transaction_currencies,id',
- 'currency_code' => 'min:3|max:51|exists:transaction_currencies,code',
- 'notes' => 'nullable|min:0|max:32768',
+ 'start' => 'required|before:end|date',
+ 'end' => 'required|after:start|date',
+ 'amount' => ['required', new IsValidPositiveAmount()],
+ 'currency_id' => 'numeric|exists:transaction_currencies,id',
+ 'currency_code' => 'min:3|max:51|exists:transaction_currencies,code',
+ 'notes' => 'nullable|min:0|max:32768',
+
+ // webhooks
+ 'fire_webhooks' => [new IsBoolean()],
];
}
+
+ /**
+ * Configure the validator instance.
+ */
+ public function withValidator(Validator $validator): void
+ {
+ $budget = $this->route()->parameter('budget');
+ $validator->after(
+ static function (Validator $validator) use ($budget): void {
+ if (0 !== count($validator->failed())) {
+ return;
+ }
+ $data = $validator->getData();
+
+ // if no currency has been provided, use the user's default currency:
+ /** @var TransactionCurrencyFactory $factory */
+ $factory = app(TransactionCurrencyFactory::class);
+ $currency = $factory->find($data['currency_id'] ?? null, $data['currency_code'] ?? null);
+ if (null === $currency) {
+ $currency = Amount::getPrimaryCurrency();
+ }
+ $currency->enabled = true;
+ $currency->save();
+
+ // validator already concluded start and end are valid dates:
+ $start = Carbon::parse($data['start'], config('app.timezone'));
+ $end = Carbon::parse($data['end'], config('app.timezone'));
+
+ // find limit with same date range and currency.
+ $limit = $budget->budgetlimits()
+ ->where('budget_limits.start_date', $start->format('Y-m-d'))
+ ->where('budget_limits.end_date', $end->format('Y-m-d'))
+ ->where('budget_limits.transaction_currency_id', $currency->id)
+ ->first(['budget_limits.*'])
+ ;
+ if (null !== $limit) {
+ $validator->errors()->add('start', trans('validation.limit_exists'));
+ }
+ }
+ );
+ if ($validator->fails()) {
+ Log::channel('audit')->error(sprintf('Validation errors in %s', self::class), $validator->errors()->toArray());
+ }
+ }
}
diff --git a/app/Api/V1/Requests/Models/BudgetLimit/UpdateRequest.php b/app/Api/V1/Requests/Models/BudgetLimit/UpdateRequest.php
index 42f7849292..5262ab4427 100644
--- a/app/Api/V1/Requests/Models/BudgetLimit/UpdateRequest.php
+++ b/app/Api/V1/Requests/Models/BudgetLimit/UpdateRequest.php
@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\BudgetLimit;
+use FireflyIII\Rules\IsBoolean;
use Illuminate\Validation\Validator;
use Carbon\Carbon;
use FireflyIII\Rules\IsValidPositiveAmount;
@@ -46,12 +47,15 @@ class UpdateRequest extends FormRequest
public function getAll(): array
{
$fields = [
- 'start' => ['start', 'date'],
- 'end' => ['end', 'date'],
- 'amount' => ['amount', 'convertString'],
- 'currency_id' => ['currency_id', 'convertInteger'],
- 'currency_code' => ['currency_code', 'convertString'],
- 'notes' => ['notes', 'stringWithNewlines'],
+ 'start' => ['start', 'date'],
+ 'end' => ['end', 'date'],
+ 'amount' => ['amount', 'convertString'],
+ 'currency_id' => ['currency_id', 'convertInteger'],
+ 'currency_code' => ['currency_code', 'convertString'],
+ 'notes' => ['notes', 'stringWithNewlines'],
+
+ // webhooks
+ 'fire_webhooks' => ['fire_webhooks', 'boolean'],
];
if (false === $this->has('notes')) {
// ignore notes, not submitted.
@@ -67,12 +71,15 @@ class UpdateRequest extends FormRequest
public function rules(): array
{
return [
- 'start' => 'date|after:1970-01-02|before:2038-01-17',
- 'end' => 'date|after:1970-01-02|before:2038-01-17',
- 'amount' => ['nullable', new IsValidPositiveAmount()],
- 'currency_id' => 'numeric|exists:transaction_currencies,id',
- 'currency_code' => 'min:3|max:51|exists:transaction_currencies,code',
- 'notes' => 'nullable|min:0|max:32768',
+ 'start' => 'date|after:1970-01-02|before:2038-01-17',
+ 'end' => 'date|after:1970-01-02|before:2038-01-17',
+ 'amount' => ['nullable', new IsValidPositiveAmount()],
+ 'currency_id' => 'numeric|exists:transaction_currencies,id',
+ 'currency_code' => 'min:3|max:51|exists:transaction_currencies,code',
+ 'notes' => 'nullable|min:0|max:32768',
+
+ // webhooks
+ 'fire_webhooks' => [new IsBoolean()],
];
}
diff --git a/app/Api/V1/Requests/Models/Transaction/StoreRequest.php b/app/Api/V1/Requests/Models/Transaction/StoreRequest.php
index a1a20fe4d1..4ad8a851cb 100644
--- a/app/Api/V1/Requests/Models/Transaction/StoreRequest.php
+++ b/app/Api/V1/Requests/Models/Transaction/StoreRequest.php
@@ -183,6 +183,7 @@ class StoreRequest extends FormRequest
// basic fields for group:
'group_title' => 'min:1|max:1000|nullable',
'error_if_duplicate_hash' => [new IsBoolean()],
+ 'fire_webhooks' => [new IsBoolean()],
'apply_rules' => [new IsBoolean()],
// location rules
diff --git a/app/Casts/SeparateTimezoneCaster.php b/app/Casts/SeparateTimezoneCaster.php
index 67a56ae83e..608f5fc1c9 100644
--- a/app/Casts/SeparateTimezoneCaster.php
+++ b/app/Casts/SeparateTimezoneCaster.php
@@ -28,7 +28,6 @@ namespace FireflyIII\Casts;
use Carbon\Carbon;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
-use Illuminate\Support\Facades\Log;
/**
* Class SeparateTimezoneCaster
diff --git a/app/Console/Commands/Correction/CorrectsAccountTypes.php b/app/Console/Commands/Correction/CorrectsAccountTypes.php
index c56fc6f79a..f554197c18 100644
--- a/app/Console/Commands/Correction/CorrectsAccountTypes.php
+++ b/app/Console/Commands/Correction/CorrectsAccountTypes.php
@@ -29,12 +29,15 @@ use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Factory\AccountFactory;
+use FireflyIII\Models\Account;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
+use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\JoinClause;
+use Illuminate\Support\Facades\Log;
class CorrectsAccountTypes extends Command
{
@@ -45,6 +48,7 @@ class CorrectsAccountTypes extends Command
private int $count;
private array $expected;
private AccountFactory $factory;
+ private AccountRepositoryInterface $repository;
/**
* Execute the console command.
@@ -110,7 +114,7 @@ class CorrectsAccountTypes extends Command
if ($resultSet->count() > 0) {
$this->friendlyLine(sprintf('Found %d journals that need to be fixed.', $resultSet->count()));
foreach ($resultSet as $entry) {
- app('log')->debug(sprintf('Now fixing journal #%d', $entry->id));
+ Log::debug(sprintf('Now fixing journal #%d', $entry->id));
/** @var null|TransactionJournal $journal */
$journal = TransactionJournal::find($entry->id);
@@ -120,7 +124,7 @@ class CorrectsAccountTypes extends Command
}
}
if (0 !== $this->count) {
- app('log')->debug(sprintf('%d journals had to be fixed.', $this->count));
+ Log::debug(sprintf('%d journals had to be fixed.', $this->count));
$this->friendlyInfo(sprintf('Acted on %d transaction(s)', $this->count));
}
@@ -134,10 +138,10 @@ class CorrectsAccountTypes extends Command
private function inspectJournal(TransactionJournal $journal): void
{
- app('log')->debug(sprintf('Now inspecting journal #%d', $journal->id));
+ Log::debug(sprintf('Now inspecting journal #%d', $journal->id));
$transactions = $journal->transactions()->count();
if (2 !== $transactions) {
- app('log')->debug(sprintf('Journal has %d transactions, so can\'t fix.', $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;
@@ -151,20 +155,20 @@ class CorrectsAccountTypes extends Command
$destAccountType = $destAccount->accountType->type;
if (!array_key_exists($type, $this->expected)) {
- app('log')->info(sprintf('No source/destination info for transaction type %s.', $type));
+ 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])) {
- app('log')->debug(sprintf('[a] Going to fix journal #%d', $journal->id));
+ 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)) {
- app('log')->debug(sprintf('[b] Going to fix journal #%d', $journal->id));
+ Log::debug(sprintf('[b] Going to fix journal #%d', $journal->id));
$this->fixJournal($journal, $type, $sourceTransaction, $destTransaction);
}
}
@@ -181,13 +185,15 @@ class CorrectsAccountTypes extends Command
private function fixJournal(TransactionJournal $journal, string $transactionType, Transaction $source, Transaction $dest): void
{
- app('log')->debug(sprintf('Going to fix journal #%d', $journal->id));
+ Log::debug(sprintf('Going to fix journal #%d', $journal->id));
+ $this->repository = app(AccountRepositoryInterface::class);
+ $this->repository->setUser($journal->user);
++$this->count;
// variables:
- $sourceType = $source->account->accountType->type;
- $destinationType = $dest->account->accountType->type;
- $combination = sprintf('%s%s%s', $transactionType, $source->account->accountType->type, $dest->account->accountType->type);
- app('log')->debug(sprintf('Combination is "%s"', $combination));
+ $sourceType = $source->account->accountType->type;
+ $destinationType = $dest->account->accountType->type;
+ $combination = sprintf('%s%s%s', $transactionType, $source->account->accountType->type, $dest->account->accountType->type);
+ Log::debug(sprintf('Combination is "%s"', $combination));
if ($this->shouldBeTransfer($transactionType, $sourceType, $destinationType)) {
$this->makeTransfer($journal);
@@ -211,37 +217,45 @@ class CorrectsAccountTypes extends Command
}
// transaction has no valid source.
- $validSources = array_keys($this->expected[$transactionType]);
- $canCreateSource = $this->canCreateSource($validSources);
- $hasValidSource = $this->hasValidAccountType($validSources, $sourceType);
+ $validSources = array_keys($this->expected[$transactionType]);
+ $canCreateSource = $this->canCreateSource($validSources);
+ $hasValidSource = $this->hasValidAccountType($validSources, $sourceType);
if (!$hasValidSource && $canCreateSource) {
$this->giveNewRevenue($journal, $source);
return;
}
if (!$canCreateSource && !$hasValidSource) {
- app('log')->debug('This transaction type has no source we can create. Just give error.');
+ Log::debug('This transaction type has no source we can create. Just give error.');
$message = sprintf('The source account of %s #%d cannot be of type "%s". Firefly III cannot fix this. You may have to remove the transaction yourself.', $transactionType, $journal->id, $source->account->accountType->type);
$this->friendlyError($message);
- app('log')->debug($message);
+ Log::debug($message);
return;
}
/** @var array $validDestinations */
- $validDestinations = $this->expected[$transactionType][$sourceType] ?? [];
- $canCreateDestination = $this->canCreateDestination($validDestinations);
- $hasValidDestination = $this->hasValidAccountType($validDestinations, $destinationType);
+ $validDestinations = $this->expected[$transactionType][$sourceType] ?? [];
+ $canCreateDestination = $this->canCreateDestination($validDestinations);
+ $hasValidDestination = $this->hasValidAccountType($validDestinations, $destinationType);
+ $alternativeDestination = $this->repository->findByName($dest->account->name, $validDestinations);
if (!$hasValidDestination && $canCreateDestination) {
$this->giveNewExpense($journal, $dest);
return;
}
- if (!$canCreateDestination && !$hasValidDestination) {
- app('log')->debug('This transaction type has no destination we can create. Just give error.');
+ if (!$canCreateDestination && !$hasValidDestination && null === $alternativeDestination) {
+ Log::debug('This transaction type has no destination we can create. Just give error.');
$message = sprintf('The destination account of %s #%d cannot be of type "%s". Firefly III cannot fix this. You may have to remove the transaction yourself.', $transactionType, $journal->id, $dest->account->accountType->type);
$this->friendlyError($message);
- app('log')->debug($message);
+ Log::debug($message);
+ }
+ if (!$canCreateDestination && !$hasValidDestination && null !== $alternativeDestination) {
+ Log::debug('This transaction type has no destination we can create, but found alternative with the same name.');
+ $message = sprintf('The destination account of %s #%d cannot be of type "%s". Firefly III found an alternative account. Please make sure this transaction is correct.', $transactionType, $journal->transaction_group_id, $dest->account->accountType->type);
+ $this->friendlyInfo($message);
+ Log::debug($message);
+ $this->giveNewDestinationAccount($journal, $alternativeDestination);
}
}
@@ -263,7 +277,7 @@ class CorrectsAccountTypes extends Command
$journal->save();
$message = sprintf('Converted transaction #%d from a transfer to a withdrawal.', $journal->id);
$this->friendlyInfo($message);
- app('log')->debug($message);
+ Log::debug($message);
// check it again:
$this->inspectJournal($journal);
}
@@ -281,7 +295,7 @@ class CorrectsAccountTypes extends Command
$journal->save();
$message = sprintf('Converted transaction #%d from a transfer to a deposit.', $journal->id);
$this->friendlyInfo($message);
- app('log')->debug($message);
+ Log::debug($message);
// check it again:
$this->inspectJournal($journal);
}
@@ -308,7 +322,7 @@ class CorrectsAccountTypes extends Command
$result->name
);
$this->friendlyWarning($message);
- app('log')->debug($message);
+ Log::debug($message);
$this->inspectJournal($journal);
}
@@ -335,7 +349,7 @@ class CorrectsAccountTypes extends Command
$result->name
);
$this->friendlyWarning($message);
- app('log')->debug($message);
+ Log::debug($message);
$this->inspectJournal($journal);
}
@@ -354,14 +368,14 @@ class CorrectsAccountTypes extends Command
private function giveNewRevenue(TransactionJournal $journal, Transaction $source): void
{
- app('log')->debug(sprintf('An account of type "%s" could be a valid source.', AccountTypeEnum::REVENUE->value));
+ 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));
- app('log')->debug(sprintf('Associated account #%d with transaction #%d', $newSource->id, $source->id));
+ Log::debug(sprintf('Associated account #%d with transaction #%d', $newSource->id, $source->id));
$this->inspectJournal($journal);
}
@@ -372,14 +386,33 @@ class CorrectsAccountTypes extends Command
private function giveNewExpense(TransactionJournal $journal, Transaction $destination): void
{
- app('log')->debug(sprintf('An account of type "%s" could be a valid destination.', AccountTypeEnum::EXPENSE->value));
+ 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));
- app('log')->debug(sprintf('Associated account #%d with transaction #%d', $newDestination->id, $destination->id));
+ 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
+ );
+ $this->friendlyInfo($message);
+ $journal->refresh();
+ Log::debug($message);
+ }
}
diff --git a/app/Console/Commands/Correction/CorrectsDatabase.php b/app/Console/Commands/Correction/CorrectsDatabase.php
index 5850619f61..4288a4af40 100644
--- a/app/Console/Commands/Correction/CorrectsDatabase.php
+++ b/app/Console/Commands/Correction/CorrectsDatabase.php
@@ -75,7 +75,8 @@ class CorrectsDatabase extends Command
'correction:recalculates-liabilities',
'correction:preferences',
// 'correction:transaction-types', // resource heavy, disabled.
- 'correction:recalculate-pc-amounts', // not necessary, disabled.
+ 'correction:recalculate-pc-amounts',
+ 'correction:remove-links-to-deleted-objects',
'firefly-iii:report-integrity',
];
foreach ($commands as $command) {
diff --git a/app/Console/Commands/Correction/RemovesLinksToDeletedObjects.php b/app/Console/Commands/Correction/RemovesLinksToDeletedObjects.php
new file mode 100644
index 0000000000..7b174418b0
--- /dev/null
+++ b/app/Console/Commands/Correction/RemovesLinksToDeletedObjects.php
@@ -0,0 +1,118 @@
+.
+ */
+
+namespace FireflyIII\Console\Commands\Correction;
+
+use FireflyIII\Console\Commands\ShowsFriendlyMessages;
+use FireflyIII\Models\Budget;
+use FireflyIII\Models\Category;
+use FireflyIII\Models\Tag;
+use FireflyIII\Models\TransactionJournal;
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\DB;
+
+class RemovesLinksToDeletedObjects extends Command
+{
+ use ShowsFriendlyMessages;
+
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'correction:remove-links-to-deleted-objects';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Removes deleted entries from intermediate tables.';
+
+ /**
+ * Execute the console command.
+ */
+ public function handle(): void
+ {
+ $deletedTags = Tag::withTrashed()->whereNotNull('deleted_at')->get('tags.id')->pluck('id')->toArray();
+ $deletedJournals = TransactionJournal::withTrashed()->whereNotNull('deleted_at')->get('transaction_journals.id')->pluck('id')->toArray();
+ $deletedBudgets = Budget::withTrashed()->whereNotNull('deleted_at')->get('budgets.id')->pluck('id')->toArray();
+ $deletedCategories = Category::withTrashed()->whereNotNull('deleted_at')->get('categories.id')->pluck('id')->toArray();
+
+ if (count($deletedTags) > 0) {
+ $this->cleanupTags($deletedTags);
+ }
+ if (count($deletedJournals) > 0) {
+ $this->cleanupJournals($deletedJournals);
+ }
+ if (count($deletedBudgets) > 0) {
+ $this->cleanupBudgets($deletedBudgets);
+ }
+ if (count($deletedCategories) > 0) {
+ $this->cleanupCategories($deletedCategories);
+ }
+ $this->friendlyNeutral('Validated links to deleted objects.');
+
+
+ }
+
+ private function cleanupTags(array $tags): void
+ {
+ $count = DB::table('tag_transaction_journal')->whereIn('tag_id', $tags)->delete();
+ if ($count > 0) {
+ $this->friendlyInfo(sprintf('Removed %d old relationship(s) categories transactions and tags.', $count));
+ }
+ }
+
+ private function cleanupJournals(array $journals): void
+ {
+ $count = DB::table('tag_transaction_journal')->whereIn('transaction_journal_id', $journals)->delete();
+ if ($count > 0) {
+ $this->friendlyInfo(sprintf('Removed %d old relationship(s) between tags and transactions.', $count));
+ }
+ $count = DB::table('budget_transaction_journal')->whereIn('transaction_journal_id', $journals)->delete();
+ if ($count > 0) {
+ $this->friendlyInfo(sprintf('Removed %d old relationship(s) between budgets and transactions.', $count));
+ }
+ $count = DB::table('category_transaction_journal')->whereIn('transaction_journal_id', $journals)->delete();
+ if ($count > 0) {
+ $this->friendlyInfo(sprintf('Removed %d old relationship(s) categories and transactions.', $count));
+ }
+ }
+
+ private function cleanupBudgets(array $budgets): void
+ {
+ $count = DB::table('budget_transaction_journal')->whereIn('budget_id', $budgets)->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));
+ }
+ }
+}
diff --git a/app/Console/Commands/Tools/ApplyRules.php b/app/Console/Commands/Tools/ApplyRules.php
index b2a891b5d6..6a42ac0990 100644
--- a/app/Console/Commands/Tools/ApplyRules.php
+++ b/app/Console/Commands/Tools/ApplyRules.php
@@ -283,7 +283,7 @@ class ApplyRules extends Command
if (null !== $endString && '' !== $endString) {
$inputEnd = Carbon::createFromFormat('Y-m-d', $endString);
}
- if (null === $inputEnd || null === $inputStart) {
+ if (!$inputEnd instanceof Carbon || null === $inputStart) {
Log::error('Could not parse start or end date in verifyInputDate().');
return;
diff --git a/app/Exceptions/GracefulNotFoundHandler.php b/app/Exceptions/GracefulNotFoundHandler.php
index fe0ca9c896..b2feef6ea6 100644
--- a/app/Exceptions/GracefulNotFoundHandler.php
+++ b/app/Exceptions/GracefulNotFoundHandler.php
@@ -86,6 +86,7 @@ class GracefulNotFoundHandler extends ExceptionHandler
return $this->handleAttachment($request, $e);
case 'bills.show':
+ case 'subscriptions.show':
$request->session()->reflash();
return redirect(route('bills.index'));
diff --git a/app/Factory/TransactionJournalFactory.php b/app/Factory/TransactionJournalFactory.php
index 7206947be2..dd2d9e6830 100644
--- a/app/Factory/TransactionJournalFactory.php
+++ b/app/Factory/TransactionJournalFactory.php
@@ -222,7 +222,7 @@ class TransactionJournalFactory
Log::debug('Source info:', $sourceInfo);
Log::debug('Destination info:', $destInfo);
$sourceAccount = $this->getAccount($type->type, 'source', $sourceInfo);
- $destinationAccount = $this->getAccount($type->type, 'destination', $destInfo);
+ $destinationAccount = $this->getAccount($type->type, 'destination', $destInfo, $sourceAccount);
Log::debug('Done with getAccount(2x)');
diff --git a/app/Handlers/Events/StoredGroupEventHandler.php b/app/Handlers/Events/StoredGroupEventHandler.php
index 367c4b4d09..50ce62f742 100644
--- a/app/Handlers/Events/StoredGroupEventHandler.php
+++ b/app/Handlers/Events/StoredGroupEventHandler.php
@@ -28,6 +28,7 @@ use FireflyIII\Events\RequestedSendWebhookMessages;
use FireflyIII\Events\StoredTransactionGroup;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\TransactionJournal;
+use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface;
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
use FireflyIII\Services\Internal\Support\CreditRecalculateService;
use FireflyIII\TransactionRules\Engine\RuleEngineInterface;
@@ -36,6 +37,8 @@ use Illuminate\Support\Facades\Log;
/**
* Class StoredGroupEventHandler
+ *
+ * TODO migrate to observer?
*/
class StoredGroupEventHandler
{
@@ -44,6 +47,7 @@ class StoredGroupEventHandler
$this->processRules($event);
$this->recalculateCredit($event);
$this->triggerWebhooks($event);
+ $this->removePeriodStatistics($event);
}
/**
@@ -94,6 +98,26 @@ class StoredGroupEventHandler
$object->recalculate();
}
+ private function removePeriodStatistics(StoredTransactionGroup $event): void
+ {
+ /** @var PeriodStatisticRepositoryInterface $repository */
+ $repository = app(PeriodStatisticRepositoryInterface::class);
+
+ /** @var TransactionJournal $journal */
+ foreach ($event->transactionGroup->transactionJournals as $journal) {
+ $source = $journal->transactions()->where('amount', '<', '0')->first();
+ $dest = $journal->transactions()->where('amount', '>', '0')->first();
+ $repository->deleteStatisticsForModel($source->account, $journal->date);
+ $repository->deleteStatisticsForModel($dest->account, $journal->date);
+ foreach ($journal->categories as $category) {
+ $repository->deleteStatisticsForModel($category, $journal->date);
+ }
+ foreach ($journal->tags as $tag) {
+ $repository->deleteStatisticsForModel($tag, $journal->date);
+ }
+ }
+ }
+
/**
* This method processes all webhooks that respond to the "stored transaction group" trigger (100)
*/
diff --git a/app/Handlers/Events/UpdatedGroupEventHandler.php b/app/Handlers/Events/UpdatedGroupEventHandler.php
index 973442abb3..e1393a3355 100644
--- a/app/Handlers/Events/UpdatedGroupEventHandler.php
+++ b/app/Handlers/Events/UpdatedGroupEventHandler.php
@@ -31,6 +31,7 @@ use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\Account;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
+use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface;
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
use FireflyIII\Services\Internal\Support\CreditRecalculateService;
use FireflyIII\Support\Models\AccountBalanceCalculator;
@@ -49,10 +50,35 @@ class UpdatedGroupEventHandler
$this->processRules($event);
$this->recalculateCredit($event);
$this->triggerWebhooks($event);
+ $this->removePeriodStatistics($event);
if ($event->runRecalculations) {
$this->updateRunningBalance($event);
}
+
+ }
+
+ /**
+ * TODO duplicate
+ */
+ private function removePeriodStatistics(UpdatedTransactionGroup $event): void
+ {
+ /** @var PeriodStatisticRepositoryInterface $repository */
+ $repository = app(PeriodStatisticRepositoryInterface::class);
+
+ /** @var TransactionJournal $journal */
+ foreach ($event->transactionGroup->transactionJournals as $journal) {
+ $source = $journal->transactions()->where('amount', '<', '0')->first();
+ $dest = $journal->transactions()->where('amount', '>', '0')->first();
+ $repository->deleteStatisticsForModel($source->account, $journal->date);
+ $repository->deleteStatisticsForModel($dest->account, $journal->date);
+ foreach ($journal->categories as $category) {
+ $repository->deleteStatisticsForModel($category, $journal->date);
+ }
+ foreach ($journal->tags as $tag) {
+ $repository->deleteStatisticsForModel($tag, $journal->date);
+ }
+ }
}
/**
diff --git a/app/Handlers/Events/WebhookEventHandler.php b/app/Handlers/Events/WebhookEventHandler.php
index adad80b942..bab7d51363 100644
--- a/app/Handlers/Events/WebhookEventHandler.php
+++ b/app/Handlers/Events/WebhookEventHandler.php
@@ -62,5 +62,8 @@ class WebhookEventHandler
Log::debug(sprintf('Skip message #%d', $message->id));
}
}
+
+ // clean up sent messages table:
+ WebhookMessage::where('webhook_messages.sent', true)->where('webhook_messages.created_at', '<', now()->subDays(30))->delete();
}
}
diff --git a/app/Handlers/Observer/BudgetLimitObserver.php b/app/Handlers/Observer/BudgetLimitObserver.php
index ae6b1aac6a..3c6558f0e9 100644
--- a/app/Handlers/Observer/BudgetLimitObserver.php
+++ b/app/Handlers/Observer/BudgetLimitObserver.php
@@ -31,6 +31,7 @@ use FireflyIII\Models\BudgetLimit;
use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\Support\Observers\RecalculatesAvailableBudgetsTrait;
+use FireflyIII\Support\Singleton\PreferencesSingleton;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
@@ -44,17 +45,24 @@ class BudgetLimitObserver
$this->updatePrimaryCurrencyAmount($budgetLimit);
$this->updateAvailableBudget($budgetLimit);
- $user = $budgetLimit->budget->user;
- /** @var MessageGeneratorInterface $engine */
- $engine = app(MessageGeneratorInterface::class);
- $engine->setUser($user);
- $engine->setObjects(new Collection()->push($budgetLimit));
- $engine->setTrigger(WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT);
- $engine->generateMessages();
+ // this is a lame trick to communicate with the observer.
+ $singleton = PreferencesSingleton::getInstance();
- Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
- event(new RequestedSendWebhookMessages());
+ if (true === $singleton->getPreference('fire_webhooks_bl_store')) {
+
+ $user = $budgetLimit->budget->user;
+
+ /** @var MessageGeneratorInterface $engine */
+ $engine = app(MessageGeneratorInterface::class);
+ $engine->setUser($user);
+ $engine->setObjects(new Collection()->push($budgetLimit));
+ $engine->setTrigger(WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT);
+ $engine->generateMessages();
+
+ Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
+ event(new RequestedSendWebhookMessages());
+ }
}
private function updatePrimaryCurrencyAmount(BudgetLimit $budgetLimit): void
@@ -82,16 +90,21 @@ class BudgetLimitObserver
$this->updatePrimaryCurrencyAmount($budgetLimit);
$this->updateAvailableBudget($budgetLimit);
- $user = $budgetLimit->budget->user;
+ // this is a lame trick to communicate with the observer.
+ $singleton = PreferencesSingleton::getInstance();
- /** @var MessageGeneratorInterface $engine */
- $engine = app(MessageGeneratorInterface::class);
- $engine->setUser($user);
- $engine->setObjects(new Collection()->push($budgetLimit));
- $engine->setTrigger(WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT);
- $engine->generateMessages();
+ if (true === $singleton->getPreference('fire_webhooks_bl_update')) {
+ $user = $budgetLimit->budget->user;
- Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
- event(new RequestedSendWebhookMessages());
+ /** @var MessageGeneratorInterface $engine */
+ $engine = app(MessageGeneratorInterface::class);
+ $engine->setUser($user);
+ $engine->setObjects(new Collection()->push($budgetLimit));
+ $engine->setTrigger(WebhookTrigger::STORE_UPDATE_BUDGET_LIMIT);
+ $engine->generateMessages();
+
+ Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
+ event(new RequestedSendWebhookMessages());
+ }
}
}
diff --git a/app/Handlers/Observer/BudgetObserver.php b/app/Handlers/Observer/BudgetObserver.php
index d7366d3a4b..2a78e1f4be 100644
--- a/app/Handlers/Observer/BudgetObserver.php
+++ b/app/Handlers/Observer/BudgetObserver.php
@@ -31,6 +31,7 @@ use FireflyIII\Models\Budget;
use FireflyIII\Models\BudgetLimit;
use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface;
use FireflyIII\Support\Observers\RecalculatesAvailableBudgetsTrait;
+use FireflyIII\Support\Singleton\PreferencesSingleton;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
@@ -45,32 +46,43 @@ class BudgetObserver
{
Log::debug(sprintf('Observe "created" of budget #%d ("%s").', $budget->id, $budget->name));
- // fire event.
- $user = $budget->user;
+ // this is a lame trick to communicate with the observer.
+ $singleton = PreferencesSingleton::getInstance();
- /** @var MessageGeneratorInterface $engine */
- $engine = app(MessageGeneratorInterface::class);
- $engine->setUser($user);
- $engine->setObjects(new Collection()->push($budget));
- $engine->setTrigger(WebhookTrigger::STORE_BUDGET);
- $engine->generateMessages();
- Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
- event(new RequestedSendWebhookMessages());
+ if (true === $singleton->getPreference('fire_webhooks_budget_create')) {
+ // fire event.
+ $user = $budget->user;
+
+ /** @var MessageGeneratorInterface $engine */
+ $engine = app(MessageGeneratorInterface::class);
+ $engine->setUser($user);
+ $engine->setObjects(new Collection()->push($budget));
+ $engine->setTrigger(WebhookTrigger::STORE_BUDGET);
+ $engine->generateMessages();
+ Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
+ event(new RequestedSendWebhookMessages());
+ }
}
public function updated(Budget $budget): void
{
Log::debug(sprintf('Observe "updated" of budget #%d ("%s").', $budget->id, $budget->name));
- $user = $budget->user;
- /** @var MessageGeneratorInterface $engine */
- $engine = app(MessageGeneratorInterface::class);
- $engine->setUser($user);
- $engine->setObjects(new Collection()->push($budget));
- $engine->setTrigger(WebhookTrigger::UPDATE_BUDGET);
- $engine->generateMessages();
- Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
- event(new RequestedSendWebhookMessages());
+ // this is a lame trick to communicate with the observer.
+ $singleton = PreferencesSingleton::getInstance();
+
+ if (true === $singleton->getPreference('fire_webhooks_budget_update')) {
+ $user = $budget->user;
+
+ /** @var MessageGeneratorInterface $engine */
+ $engine = app(MessageGeneratorInterface::class);
+ $engine->setUser($user);
+ $engine->setObjects(new Collection()->push($budget));
+ $engine->setTrigger(WebhookTrigger::UPDATE_BUDGET);
+ $engine->generateMessages();
+ Log::debug(sprintf('send event RequestedSendWebhookMessages from %s', __METHOD__));
+ event(new RequestedSendWebhookMessages());
+ }
}
public function deleting(Budget $budget): void
diff --git a/app/Http/Controllers/Account/ShowController.php b/app/Http/Controllers/Account/ShowController.php
index 4b77635b4b..8c9ac76727 100644
--- a/app/Http/Controllers/Account/ShowController.php
+++ b/app/Http/Controllers/Account/ShowController.php
@@ -102,7 +102,7 @@ class ShowController extends Controller
// make sure dates are end of day and start of day:
$start->startOfDay();
- $end->endOfDay();
+ $end->endOfDay()->milli(0);
$location = $this->repository->getLocation($account);
$attachments = $this->repository->getAttachments($account);
diff --git a/app/Http/Controllers/Admin/NotificationController.php b/app/Http/Controllers/Admin/NotificationController.php
index b8e52ebbe6..233dc7f66a 100644
--- a/app/Http/Controllers/Admin/NotificationController.php
+++ b/app/Http/Controllers/Admin/NotificationController.php
@@ -122,6 +122,11 @@ class NotificationController extends Controller
public function testNotification(Request $request): RedirectResponse
{
+ if (true === auth()->user()->hasRole('demo')) {
+ session()->flash('error', (string) trans('firefly.not_available_demo_user'));
+
+ return redirect(route('settings.notification.index'));
+ }
$all = $request->all();
$channel = $all['test_submit'] ?? '';
diff --git a/app/Http/Controllers/Admin/UpdateController.php b/app/Http/Controllers/Admin/UpdateController.php
index 8502d43ce8..90729abe56 100644
--- a/app/Http/Controllers/Admin/UpdateController.php
+++ b/app/Http/Controllers/Admin/UpdateController.php
@@ -27,6 +27,7 @@ use Carbon\Carbon;
use FireflyIII\Helpers\Update\UpdateTrait;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Middleware\IsDemoUser;
+use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -66,8 +67,8 @@ class UpdateController extends Controller
{
$subTitle = (string) trans('firefly.update_check_title');
$subTitleIcon = 'fa-star';
- $permission = app('fireflyconfig')->get('permission_update_check', -1);
- $channel = app('fireflyconfig')->get('update_channel', 'stable');
+ $permission = FireflyConfig::get('permission_update_check', -1);
+ $channel = FireflyConfig::get('update_channel', 'stable');
$selected = $permission->data;
$channelSelected = $channel->data;
$options = [
@@ -96,9 +97,9 @@ class UpdateController extends Controller
$channel = $request->get('update_channel');
$channel = in_array($channel, ['stable', 'beta', 'alpha'], true) ? $channel : 'stable';
- app('fireflyconfig')->set('permission_update_check', $checkForUpdates);
- app('fireflyconfig')->set('last_update_check', Carbon::now()->getTimestamp());
- app('fireflyconfig')->set('update_channel', $channel);
+ FireflyConfig::set('permission_update_check', $checkForUpdates);
+ FireflyConfig::set('last_update_check', Carbon::now()->getTimestamp());
+ FireflyConfig::set('update_channel', $channel);
session()->flash('success', (string) trans('firefly.configuration_updated'));
return redirect(route('settings.update-check'));
diff --git a/app/Http/Controllers/Budget/ShowController.php b/app/Http/Controllers/Budget/ShowController.php
index 36815436a4..ce344f13e8 100644
--- a/app/Http/Controllers/Budget/ShowController.php
+++ b/app/Http/Controllers/Budget/ShowController.php
@@ -92,7 +92,7 @@ class ShowController extends Controller
// get first journal ever to set off the budget period overview.
$first = $this->journalRepos->firstNull();
$firstDate = $first instanceof TransactionJournal ? $first->date : $start;
- $periods = $this->getNoBudgetPeriodOverview($firstDate, $end);
+ $periods = $this->getNoModelPeriodOverview('budget', $firstDate, $end);
$page = (int) $request->get('page');
$pageSize = (int) app('preferences')->get('listPageSize', 50)->data;
diff --git a/app/Http/Controllers/Category/NoCategoryController.php b/app/Http/Controllers/Category/NoCategoryController.php
index be784e6aa6..4c12faeef3 100644
--- a/app/Http/Controllers/Category/NoCategoryController.php
+++ b/app/Http/Controllers/Category/NoCategoryController.php
@@ -35,6 +35,7 @@ use FireflyIII\Support\Http\Controllers\PeriodOverview;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
/**
@@ -74,7 +75,7 @@ class NoCategoryController extends Controller
*/
public function show(Request $request, ?Carbon $start = null, ?Carbon $end = null)
{
- app('log')->debug('Start of noCategory()');
+ Log::debug('Start of noCategory()');
$start ??= session('start');
$end ??= session('end');
@@ -82,14 +83,12 @@ class NoCategoryController extends Controller
/** @var Carbon $end */
$page = (int) $request->get('page');
$pageSize = (int) app('preferences')->get('listPageSize', 50)->data;
- $subTitle = trans(
- 'firefly.without_category_between',
- ['start' => $start->isoFormat($this->monthAndDayFormat), 'end' => $end->isoFormat($this->monthAndDayFormat)]
- );
- $periods = $this->getNoCategoryPeriodOverview($start);
+ $subTitle = trans('firefly.without_category_between', ['start' => $start->isoFormat($this->monthAndDayFormat), 'end' => $end->isoFormat($this->monthAndDayFormat)]);
+ $first = $this->journalRepos->firstNull()->date ?? clone $start;
+ $periods = $this->getNoModelPeriodOverview('category', $first, $end);
- app('log')->debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d')));
- app('log')->debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d')));
+ Log::debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d')));
+ Log::debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d')));
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
@@ -117,13 +116,13 @@ class NoCategoryController extends Controller
$periods = new Collection();
$page = (int) $request->get('page');
$pageSize = (int) app('preferences')->get('listPageSize', 50)->data;
- app('log')->debug('Start of noCategory()');
+ Log::debug('Start of noCategory()');
$subTitle = (string) trans('firefly.all_journals_without_category');
$first = $this->journalRepos->firstNull();
$start = $first instanceof TransactionJournal ? $first->date : new Carbon();
$end = today(config('app.timezone'));
- app('log')->debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d')));
- app('log')->debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d')));
+ Log::debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d')));
+ Log::debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d')));
/** @var GroupCollectorInterface $collector */
$collector = app(GroupCollectorInterface::class);
diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
index 7a4c542730..0919f8c199 100644
--- a/app/Http/Controllers/Controller.php
+++ b/app/Http/Controllers/Controller.php
@@ -26,6 +26,7 @@ namespace FireflyIII\Http\Controllers;
use FireflyIII\Events\RequestedSendWebhookMessages;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Support\Facades\Amount;
+use FireflyIII\Support\Facades\Preferences;
use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Controllers\RequestInformation;
use FireflyIII\Support\Http\Controllers\UserNavigation;
@@ -133,7 +134,7 @@ abstract class Controller extends BaseController
$this->primaryCurrency = Amount::getPrimaryCurrency();
$language = Steam::getLanguage();
$locale = Steam::getLocale();
- $darkMode = app('preferences')->get('darkMode', 'browser')->data;
+ $darkMode = Preferences::get('darkMode', 'browser')->data;
$this->convertToPrimary = Amount::convertToPrimary();
$page = $this->getPageName();
$shownDemo = $this->hasSeenDemo();
diff --git a/app/Http/Controllers/DebugController.php b/app/Http/Controllers/DebugController.php
index 73141d0b8c..377bacd735 100644
--- a/app/Http/Controllers/DebugController.php
+++ b/app/Http/Controllers/DebugController.php
@@ -30,6 +30,7 @@ use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Http\Middleware\IsDemoUser;
+use FireflyIII\Models\PeriodStatistic;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
@@ -108,6 +109,8 @@ class DebugController extends Controller
Artisan::call('route:clear');
Artisan::call('view:clear');
+ PeriodStatistic::where('id', '>', 0)->delete();
+
// also do some recalculations.
Artisan::call('correction:recalculates-liabilities');
AccountBalanceCalculator::recalculateAll(false);
diff --git a/app/Http/Controllers/Transaction/CreateController.php b/app/Http/Controllers/Transaction/CreateController.php
index 519ff3d538..c69ddfc2ce 100644
--- a/app/Http/Controllers/Transaction/CreateController.php
+++ b/app/Http/Controllers/Transaction/CreateController.php
@@ -31,6 +31,7 @@ use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface;
use FireflyIII\Services\Internal\Update\GroupCloneService;
+use FireflyIII\Support\Facades\Preferences;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use Illuminate\Http\JsonResponse;
@@ -76,7 +77,7 @@ class CreateController extends Controller
// event!
event(new StoredTransactionGroup($newGroup, true, true));
- app('preferences')->mark();
+ Preferences::mark();
$title = $newGroup->title ?? $newGroup->transactionJournals->first()->description;
$link = route('transactions.show', [$newGroup->id]);
@@ -103,7 +104,7 @@ class CreateController extends Controller
* */
public function create(?string $objectType)
{
- app('preferences')->mark();
+ Preferences::mark();
$sourceId = (int) request()->get('source');
$destinationId = (int) request()->get('destination');
@@ -114,7 +115,9 @@ class CreateController extends Controller
$preFilled = session()->has('preFilled') ? session('preFilled') : [];
$subTitle = (string) trans(sprintf('breadcrumbs.create_%s', strtolower((string) $objectType)));
$subTitleIcon = 'fa-plus';
- $optionalFields = app('preferences')->get('transaction_journal_optional_fields', [])->data;
+
+ /** @var null|array $optionalFields */
+ $optionalFields = Preferences::get('transaction_journal_optional_fields', [])->data;
$allowedOpposingTypes = config('firefly.allowed_opposing_types');
$accountToTypes = config('firefly.account_to_transaction');
$previousUrl = $this->rememberPreviousUrl('transactions.create.url');
diff --git a/app/Http/Controllers/Transaction/ShowController.php b/app/Http/Controllers/Transaction/ShowController.php
index 2d9e81f511..7c2fff6105 100644
--- a/app/Http/Controllers/Transaction/ShowController.php
+++ b/app/Http/Controllers/Transaction/ShowController.php
@@ -146,25 +146,7 @@ class ShowController extends Controller
$attachments = $this->repository->getAttachments($transactionGroup);
$links = $this->repository->getLinks($transactionGroup);
- return view(
- 'transactions.show',
- compact(
- 'transactionGroup',
- 'amounts',
- 'first',
- 'type',
- 'logEntries',
- 'groupLogEntries',
- 'subTitle',
- 'splits',
- 'selectedGroup',
- 'groupArray',
- 'events',
- 'attachments',
- 'links',
- 'accounts',
- )
- );
+ return view('transactions.show', compact('transactionGroup', 'amounts', 'first', 'type', 'logEntries', 'groupLogEntries', 'subTitle', 'splits', 'selectedGroup', 'groupArray', 'events', 'attachments', 'links', 'accounts'));
}
private function getAmounts(array $group): array
diff --git a/app/Models/Account.php b/app/Models/Account.php
index 683b4a2973..76d520856d 100644
--- a/app/Models/Account.php
+++ b/app/Models/Account.php
@@ -23,11 +23,13 @@ declare(strict_types=1);
namespace FireflyIII\Models;
-use Illuminate\Database\Eloquent\Attributes\Scope;
use FireflyIII\Enums\AccountTypeEnum;
+use FireflyIII\Handlers\Observer\AccountObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
+use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -40,6 +42,7 @@ use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+#[ObservedBy([AccountObserver::class])]
class Account extends Model
{
use HasFactory;
@@ -60,7 +63,7 @@ class Account extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $accountId = (int) $value;
+ $accountId = (int)$value;
/** @var User $user */
$user = auth()->user();
@@ -95,39 +98,6 @@ class Account extends Model
return $this->morphMany(Attachment::class, 'attachable');
}
- /**
- * Get the account number.
- */
- protected function accountNumber(): Attribute
- {
- return Attribute::make(get: function () {
- /** @var null|AccountMeta $metaValue */
- $metaValue = $this->accountMeta()
- ->where('name', 'account_number')
- ->first()
- ;
-
- return null !== $metaValue ? $metaValue->data : '';
- });
- }
-
- public function accountMeta(): HasMany
- {
- return $this->hasMany(AccountMeta::class);
- }
-
- protected function editName(): Attribute
- {
- return Attribute::make(get: function () {
- $name = $this->name;
- if (AccountTypeEnum::CASH->value === $this->accountType->type) {
- return '';
- }
-
- return $name;
- });
- }
-
public function locations(): MorphMany
{
return $this->morphMany(Location::class, 'locatable');
@@ -154,19 +124,9 @@ class Account extends Model
return $this->belongsToMany(PiggyBank::class);
}
- #[Scope]
- protected function accountTypeIn(EloquentBuilder $query, array $types): void
- {
- if (false === $this->joinedAccountTypes) {
- $query->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id');
- $this->joinedAccountTypes = true;
- }
- $query->whereIn('account_types.type', $types);
- }
-
public function setVirtualBalanceAttribute(mixed $value): void
{
- $value = (string) $value;
+ $value = (string)$value;
if ('' === $value) {
$value = null;
}
@@ -186,42 +146,49 @@ class Account extends Model
protected function accountId(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (int)$value,
);
}
+ /**
+ * Get the account number.
+ */
+ protected function accountNumber(): Attribute
+ {
+ return Attribute::make(get: function () {
+ /** @var null|AccountMeta $metaValue */
+ $metaValue = $this->accountMeta()
+ ->where('name', 'account_number')
+ ->first()
+ ;
+
+ return null !== $metaValue ? $metaValue->data : '';
+ });
+ }
+
+ public function accountMeta(): HasMany
+ {
+ return $this->hasMany(AccountMeta::class);
+ }
+
/**
* Get the user ID
*/
protected function accountTypeId(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (int)$value,
);
}
- protected function iban(): Attribute
+ #[Scope]
+ protected function accountTypeIn(EloquentBuilder $query, array $types): void
{
- return Attribute::make(
- get: static fn ($value) => null === $value ? null : trim(str_replace(' ', '', (string) $value)),
- );
- }
-
- protected function order(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- /**
- * Get the virtual balance
- */
- protected function virtualBalance(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (string) $value,
- );
+ if (false === $this->joinedAccountTypes) {
+ $query->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id');
+ $this->joinedAccountTypes = true;
+ }
+ $query->whereIn('account_types.type', $types);
}
protected function casts(): array
@@ -238,4 +205,47 @@ class Account extends Model
'native_virtual_balance' => 'string',
];
}
+
+ protected function editName(): Attribute
+ {
+ return Attribute::make(get: function () {
+ $name = $this->name;
+ if (AccountTypeEnum::CASH->value === $this->accountType->type) {
+ return '';
+ }
+
+ return $name;
+ });
+ }
+
+ protected function iban(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => null === $value ? null : trim(str_replace(' ', '', (string)$value)),
+ );
+ }
+
+ protected function order(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
+ /**
+ * Get the virtual balance
+ */
+ protected function virtualBalance(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (string)$value,
+ );
+ }
+
+ public function primaryPeriodStatistics(): MorphMany
+ {
+
+ return $this->morphMany(PeriodStatistic::class, 'primary_statable');
+
+ }
}
diff --git a/app/Models/AccountMeta.php b/app/Models/AccountMeta.php
index 61d8644ff3..a91e5eb778 100644
--- a/app/Models/AccountMeta.php
+++ b/app/Models/AccountMeta.php
@@ -23,8 +23,8 @@ declare(strict_types=1);
namespace FireflyIII\Models;
-use Illuminate\Database\Eloquent\Casts\Attribute;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
+use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -43,11 +43,6 @@ class AccountMeta extends Model
return $this->belongsTo(Account::class);
}
- protected function data(): Attribute
- {
- return Attribute::make(get: fn (mixed $value) => (string) json_decode((string) $value, true), set: fn (mixed $value) => ['data' => json_encode($value)]);
- }
-
protected function casts(): array
{
return [
@@ -55,4 +50,9 @@ class AccountMeta extends Model
'updated_at' => 'datetime',
];
}
+
+ protected function data(): Attribute
+ {
+ return Attribute::make(get: fn (mixed $value) => (string)json_decode((string)$value, true), set: fn (mixed $value) => ['data' => json_encode($value)]);
+ }
}
diff --git a/app/Models/AccountType.php b/app/Models/AccountType.php
index d4571fde77..d4b82b45fd 100644
--- a/app/Models/AccountType.php
+++ b/app/Models/AccountType.php
@@ -32,46 +32,60 @@ class AccountType extends Model
{
use ReturnsIntegerIdTrait;
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string ASSET = 'Asset account';
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string BENEFICIARY = 'Beneficiary account';
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string CASH = 'Cash account';
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string CREDITCARD = 'Credit card';
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string DEBT = 'Debt';
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string DEFAULT = 'Default account';
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string EXPENSE = 'Expense account';
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string IMPORT = 'Import account';
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string INITIAL_BALANCE = 'Initial balance account';
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string LIABILITY_CREDIT = 'Liability credit account';
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string LOAN = 'Loan';
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string MORTGAGE = 'Mortgage';
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string RECONCILIATION = 'Reconciliation account';
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string REVENUE = 'Revenue account';
protected $casts
diff --git a/app/Models/Attachment.php b/app/Models/Attachment.php
index 2287666db1..0323697cb8 100644
--- a/app/Models/Attachment.php
+++ b/app/Models/Attachment.php
@@ -23,9 +23,11 @@ declare(strict_types=1);
namespace FireflyIII\Models;
+use FireflyIII\Handlers\Observer\AttachmentObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -34,6 +36,7 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+#[ObservedBy([AttachmentObserver::class])]
class Attachment extends Model
{
use ReturnsIntegerIdTrait;
@@ -50,7 +53,7 @@ class Attachment extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $attachmentId = (int) $value;
+ $attachmentId = (int)$value;
/** @var User $user */
$user = auth()->user();
@@ -83,7 +86,7 @@ class Attachment extends Model
*/
public function fileName(): string
{
- return sprintf('at-%s.data', (string) $this->id);
+ return sprintf('at-%s.data', (string)$this->id);
}
/**
@@ -97,7 +100,7 @@ class Attachment extends Model
protected function attachableId(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (int)$value,
);
}
diff --git a/app/Models/AuditLogEntry.php b/app/Models/AuditLogEntry.php
index 53773692a1..fded5ca815 100644
--- a/app/Models/AuditLogEntry.php
+++ b/app/Models/AuditLogEntry.php
@@ -48,14 +48,7 @@ class AuditLogEntry extends Model
protected function auditableId(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- protected function changerId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (int)$value,
);
}
@@ -69,4 +62,11 @@ class AuditLogEntry extends Model
'deleted_at' => 'datetime',
];
}
+
+ protected function changerId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/AutoBudget.php b/app/Models/AutoBudget.php
index 7f53584616..73dad3c6aa 100644
--- a/app/Models/AutoBudget.php
+++ b/app/Models/AutoBudget.php
@@ -25,24 +25,30 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use Deprecated;
+use FireflyIII\Handlers\Observer\AutoBudgetObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
+#[ObservedBy([AutoBudgetObserver::class])]
class AutoBudget extends Model
{
use ReturnsIntegerIdTrait;
use SoftDeletes;
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const int AUTO_BUDGET_ADJUSTED = 3;
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const int AUTO_BUDGET_RESET = 1;
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const int AUTO_BUDGET_ROLLOVER = 2;
protected $casts
= [
@@ -64,14 +70,14 @@ class AutoBudget extends Model
protected function amount(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (string) $value,
+ get: static fn ($value) => (string)$value,
);
}
protected function budgetId(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (int)$value,
);
}
@@ -85,7 +91,7 @@ class AutoBudget extends Model
protected function transactionCurrencyId(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (int)$value,
);
}
}
diff --git a/app/Models/AvailableBudget.php b/app/Models/AvailableBudget.php
index 576864a2ea..109abd0307 100644
--- a/app/Models/AvailableBudget.php
+++ b/app/Models/AvailableBudget.php
@@ -24,15 +24,18 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use Carbon\Carbon;
+use FireflyIII\Handlers\Observer\AvailableBudgetObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+#[ObservedBy([AvailableBudgetObserver::class])]
class AvailableBudget extends Model
{
use ReturnsIntegerIdTrait;
@@ -49,7 +52,7 @@ class AvailableBudget extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $availableBudgetId = (int) $value;
+ $availableBudgetId = (int)$value;
/** @var User $user */
$user = auth()->user();
@@ -77,10 +80,26 @@ class AvailableBudget extends Model
protected function amount(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (string) $value,
+ get: static fn ($value) => (string)$value,
);
}
+ protected function casts(): array
+ {
+ return [
+ 'created_at' => 'datetime',
+ 'updated_at' => 'datetime',
+ 'deleted_at' => 'datetime',
+ 'start_date' => 'date',
+ 'end_date' => 'date',
+ 'transaction_currency_id' => 'int',
+ 'amount' => 'string',
+ 'native_amount' => 'string',
+ 'user_id' => 'integer',
+ 'user_group_id' => 'integer',
+ ];
+ }
+
protected function endDate(): Attribute
{
return Attribute::make(
@@ -100,23 +119,7 @@ class AvailableBudget extends Model
protected function transactionCurrencyId(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (int)$value,
);
}
-
- protected function casts(): array
- {
- return [
- 'created_at' => 'datetime',
- 'updated_at' => 'datetime',
- 'deleted_at' => 'datetime',
- 'start_date' => 'date',
- 'end_date' => 'date',
- 'transaction_currency_id' => 'int',
- 'amount' => 'string',
- 'native_amount' => 'string',
- 'user_id' => 'integer',
- 'user_group_id' => 'integer',
- ];
- }
}
diff --git a/app/Models/Bill.php b/app/Models/Bill.php
index a0f59bc9d4..b57d98df1d 100644
--- a/app/Models/Bill.php
+++ b/app/Models/Bill.php
@@ -24,9 +24,11 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Casts\SeparateTimezoneCaster;
+use FireflyIII\Handlers\Observer\BillObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -36,6 +38,7 @@ use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+#[ObservedBy([BillObserver::class])]
class Bill extends Model
{
use ReturnsIntegerIdTrait;
@@ -75,7 +78,7 @@ class Bill extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $billId = (int) $value;
+ $billId = (int)$value;
/** @var User $user */
$user = auth()->user();
@@ -121,7 +124,7 @@ class Bill extends Model
*/
public function setAmountMaxAttribute($value): void
{
- $this->attributes['amount_max'] = (string) $value;
+ $this->attributes['amount_max'] = (string)$value;
}
/**
@@ -129,7 +132,7 @@ class Bill extends Model
*/
public function setAmountMinAttribute($value): void
{
- $this->attributes['amount_min'] = (string) $value;
+ $this->attributes['amount_min'] = (string)$value;
}
public function transactionCurrency(): BelongsTo
@@ -148,7 +151,7 @@ class Bill extends Model
protected function amountMax(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (string) $value,
+ get: static fn ($value) => (string)$value,
);
}
@@ -158,31 +161,7 @@ class Bill extends Model
protected function amountMin(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (string) $value,
- );
- }
-
- protected function order(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- /**
- * Get the skip
- */
- protected function skip(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- protected function transactionCurrencyId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (string)$value,
);
}
@@ -206,4 +185,28 @@ class Bill extends Model
'native_amount_max' => 'string',
];
}
+
+ protected function order(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
+ /**
+ * Get the skip
+ */
+ protected function skip(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
+ protected function transactionCurrencyId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/Budget.php b/app/Models/Budget.php
index c5c1c29d46..4375c6c2a7 100644
--- a/app/Models/Budget.php
+++ b/app/Models/Budget.php
@@ -23,9 +23,11 @@ declare(strict_types=1);
namespace FireflyIII\Models;
+use FireflyIII\Handlers\Observer\BudgetObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -35,6 +37,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+#[ObservedBy([BudgetObserver::class])]
class Budget extends Model
{
use ReturnsIntegerIdTrait;
@@ -53,7 +56,7 @@ class Budget extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $budgetId = (int) $value;
+ $budgetId = (int)$value;
/** @var User $user */
$user = auth()->user();
@@ -106,13 +109,6 @@ class Budget extends Model
return $this->belongsToMany(Transaction::class, 'budget_transaction', 'budget_id');
}
- protected function order(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
protected function casts(): array
{
return [
@@ -125,4 +121,11 @@ class Budget extends Model
'user_group_id' => 'integer',
];
}
+
+ protected function order(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/BudgetLimit.php b/app/Models/BudgetLimit.php
index e7270fc7b0..66f12753c6 100644
--- a/app/Models/BudgetLimit.php
+++ b/app/Models/BudgetLimit.php
@@ -24,13 +24,16 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Casts\SeparateTimezoneCaster;
+use FireflyIII\Handlers\Observer\BudgetLimitObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+#[ObservedBy([BudgetLimitObserver::class])]
class BudgetLimit extends Model
{
use ReturnsIntegerIdTrait;
@@ -45,7 +48,7 @@ class BudgetLimit extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $budgetLimitId = (int) $value;
+ $budgetLimitId = (int)$value;
$budgetLimit = self::where('budget_limits.id', $budgetLimitId)
->leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id')
->where('budgets.user_id', auth()->user()->id)
@@ -83,21 +86,14 @@ class BudgetLimit extends Model
protected function amount(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (string) $value,
+ get: static fn ($value) => (string)$value,
);
}
protected function budgetId(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- protected function transactionCurrencyId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (int)$value,
);
}
@@ -113,4 +109,11 @@ class BudgetLimit extends Model
'native_amount' => 'string',
];
}
+
+ protected function transactionCurrencyId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/Category.php b/app/Models/Category.php
index 3e50f448c6..55c9c7ebcf 100644
--- a/app/Models/Category.php
+++ b/app/Models/Category.php
@@ -24,9 +24,11 @@ declare(strict_types=1);
namespace FireflyIII\Models;
+use FireflyIII\Handlers\Observer\CategoryObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@@ -34,6 +36,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+#[ObservedBy([CategoryObserver::class])]
class Category extends Model
{
use ReturnsIntegerIdTrait;
@@ -52,7 +55,7 @@ class Category extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $categoryId = (int) $value;
+ $categoryId = (int)$value;
/** @var User $user */
$user = auth()->user();
@@ -106,4 +109,9 @@ class Category extends Model
'user_group_id' => 'integer',
];
}
+
+ public function primaryPeriodStatistics(): MorphMany
+ {
+ return $this->morphMany(PeriodStatistic::class, 'primary_statable');
+ }
}
diff --git a/app/Models/Configuration.php b/app/Models/Configuration.php
index 7ed2b13f2e..3fd2d82a19 100644
--- a/app/Models/Configuration.php
+++ b/app/Models/Configuration.php
@@ -23,8 +23,8 @@ declare(strict_types=1);
namespace FireflyIII\Models;
-use Illuminate\Database\Eloquent\Casts\Attribute;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
+use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -38,14 +38,6 @@ class Configuration extends Model
protected $table = 'configuration';
- /**
- * TODO can be replaced with native laravel code.
- */
- protected function data(): Attribute
- {
- return Attribute::make(get: fn ($value) => json_decode((string) $value), set: fn ($value) => ['data' => json_encode($value)]);
- }
-
protected function casts(): array
{
return [
@@ -54,4 +46,12 @@ class Configuration extends Model
'deleted_at' => 'datetime',
];
}
+
+ /**
+ * TODO can be replaced with native laravel code.
+ */
+ protected function data(): Attribute
+ {
+ return Attribute::make(get: fn ($value) => json_decode((string)$value), set: fn ($value) => ['data' => json_encode($value)]);
+ }
}
diff --git a/app/Models/CurrencyExchangeRate.php b/app/Models/CurrencyExchangeRate.php
index f5117715fe..a1bfa4a013 100644
--- a/app/Models/CurrencyExchangeRate.php
+++ b/app/Models/CurrencyExchangeRate.php
@@ -37,7 +37,8 @@ class CurrencyExchangeRate extends Model
use ReturnsIntegerIdTrait;
use ReturnsIntegerUserIdTrait;
use SoftDeletes;
- protected $fillable = ['user_id', 'from_currency_id', 'to_currency_id', 'date', 'date_tz', 'rate'];
+
+ protected $fillable = ['user_id', 'user_group_id', 'from_currency_id', 'to_currency_id', 'date', 'date_tz', 'rate'];
public function fromCurrency(): BelongsTo
{
@@ -54,34 +55,6 @@ class CurrencyExchangeRate extends Model
return $this->belongsTo(User::class);
}
- protected function fromCurrencyId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- protected function rate(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (string) $value,
- );
- }
-
- protected function toCurrencyId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- protected function userRate(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (string) $value,
- );
- }
-
protected function casts(): array
{
return [
@@ -96,4 +69,32 @@ class CurrencyExchangeRate extends Model
'user_rate' => 'string',
];
}
+
+ protected function fromCurrencyId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
+ protected function rate(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (string)$value,
+ );
+ }
+
+ protected function toCurrencyId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
+ protected function userRate(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (string)$value,
+ );
+ }
}
diff --git a/app/Models/GroupMembership.php b/app/Models/GroupMembership.php
index 830826967e..52f6cf9b24 100644
--- a/app/Models/GroupMembership.php
+++ b/app/Models/GroupMembership.php
@@ -53,13 +53,6 @@ class GroupMembership extends Model
return $this->belongsTo(UserRole::class);
}
- protected function userRoleId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
protected function casts(): array
{
return [
@@ -69,4 +62,11 @@ class GroupMembership extends Model
'user_group_id' => 'integer',
];
}
+
+ protected function userRoleId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/InvitedUser.php b/app/Models/InvitedUser.php
index d543d82c47..81e945b5e9 100644
--- a/app/Models/InvitedUser.php
+++ b/app/Models/InvitedUser.php
@@ -36,6 +36,7 @@ class InvitedUser extends Model
{
use ReturnsIntegerIdTrait;
use ReturnsIntegerUserIdTrait;
+
protected $fillable = ['user_group_id', 'user_id', 'email', 'invite_code', 'expires', 'expires_tz', 'redeemed'];
/**
@@ -44,7 +45,7 @@ class InvitedUser extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $attemptId = (int) $value;
+ $attemptId = (int)$value;
/** @var null|InvitedUser $attempt */
$attempt = self::find($attemptId);
diff --git a/app/Models/LinkType.php b/app/Models/LinkType.php
index f7b662961c..e399ad2e9a 100644
--- a/app/Models/LinkType.php
+++ b/app/Models/LinkType.php
@@ -44,7 +44,7 @@ class LinkType extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $linkTypeId = (int) $value;
+ $linkTypeId = (int)$value;
$linkType = self::find($linkTypeId);
if (null !== $linkType) {
return $linkType;
diff --git a/app/Models/Location.php b/app/Models/Location.php
index ce5744dba9..b8f2b31151 100644
--- a/app/Models/Location.php
+++ b/app/Models/Location.php
@@ -66,13 +66,6 @@ class Location extends Model
return $this->morphMany(TransactionJournal::class, 'locatable');
}
- protected function locatableId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
protected function casts(): array
{
return [
@@ -84,4 +77,11 @@ class Location extends Model
'longitude' => 'float',
];
}
+
+ protected function locatableId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/Note.php b/app/Models/Note.php
index 6c8c56653e..2adeacc457 100644
--- a/app/Models/Note.php
+++ b/app/Models/Note.php
@@ -44,13 +44,6 @@ class Note extends Model
return $this->morphTo();
}
- protected function noteableId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
protected function casts(): array
{
return [
@@ -59,4 +52,11 @@ class Note extends Model
'deleted_at' => 'datetime',
];
}
+
+ protected function noteableId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/ObjectGroup.php b/app/Models/ObjectGroup.php
index d7833412fb..189e543a41 100644
--- a/app/Models/ObjectGroup.php
+++ b/app/Models/ObjectGroup.php
@@ -37,6 +37,7 @@ class ObjectGroup extends Model
{
use ReturnsIntegerIdTrait;
use ReturnsIntegerUserIdTrait;
+
protected $fillable = ['title', 'order', 'user_id', 'user_group_id'];
/**
@@ -47,7 +48,7 @@ class ObjectGroup extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $objectGroupId = (int) $value;
+ $objectGroupId = (int)$value;
/** @var null|ObjectGroup $objectGroup */
$objectGroup = self::where('object_groups.id', $objectGroupId)
@@ -90,13 +91,6 @@ class ObjectGroup extends Model
return $this->morphedByMany(PiggyBank::class, 'object_groupable');
}
- protected function order(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
protected function casts(): array
{
return [
@@ -107,4 +101,11 @@ class ObjectGroup extends Model
'deleted_at' => 'datetime',
];
}
+
+ protected function order(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/PeriodStatistic.php b/app/Models/PeriodStatistic.php
new file mode 100644
index 0000000000..c8878cfc9b
--- /dev/null
+++ b/app/Models/PeriodStatistic.php
@@ -0,0 +1,60 @@
+ 'datetime',
+ 'updated_at' => 'datetime',
+ 'start' => SeparateTimezoneCaster::class,
+ 'end' => SeparateTimezoneCaster::class,
+ ];
+ }
+
+ public function userGroup(): BelongsTo
+ {
+ return $this->belongsTo(UserGroup::class);
+ }
+
+ protected function count(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
+ public function primaryStatable(): MorphTo
+ {
+
+ return $this->morphTo();
+
+ }
+
+ public function secondaryStatable(): MorphTo
+ {
+
+ return $this->morphTo();
+
+ }
+
+ public function tertiaryStatable(): MorphTo
+ {
+
+ return $this->morphTo();
+
+ }
+}
diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php
index 08d5a2563d..d4eb25a787 100644
--- a/app/Models/PiggyBank.php
+++ b/app/Models/PiggyBank.php
@@ -23,7 +23,9 @@ declare(strict_types=1);
namespace FireflyIII\Models;
+use FireflyIII\Handlers\Observer\PiggyBankObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -34,6 +36,7 @@ use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+#[ObservedBy([PiggyBankObserver::class])]
class PiggyBank extends Model
{
use ReturnsIntegerIdTrait;
@@ -49,7 +52,7 @@ class PiggyBank extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $piggyBankId = (int) $value;
+ $piggyBankId = (int)$value;
$piggyBank = self::where('piggy_banks.id', $piggyBankId)
->leftJoin('account_piggy_bank', 'account_piggy_bank.piggy_bank_id', '=', 'piggy_banks.id')
->leftJoin('accounts', 'accounts.id', '=', 'account_piggy_bank.account_id')
@@ -109,7 +112,7 @@ class PiggyBank extends Model
*/
public function setTargetAmountAttribute($value): void
{
- $this->attributes['target_amount'] = (string) $value;
+ $this->attributes['target_amount'] = (string)$value;
}
public function transactionCurrency(): BelongsTo
@@ -120,24 +123,7 @@ class PiggyBank extends Model
protected function accountId(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- protected function order(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- /**
- * Get the max amount
- */
- protected function targetAmount(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (string) $value,
+ get: static fn ($value) => (int)$value,
);
}
@@ -156,4 +142,21 @@ class PiggyBank extends Model
'native_target_amount' => 'string',
];
}
+
+ protected function order(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
+ /**
+ * Get the max amount
+ */
+ protected function targetAmount(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (string)$value,
+ );
+ }
}
diff --git a/app/Models/PiggyBankEvent.php b/app/Models/PiggyBankEvent.php
index 733792ffeb..3395e3df53 100644
--- a/app/Models/PiggyBankEvent.php
+++ b/app/Models/PiggyBankEvent.php
@@ -24,11 +24,14 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Casts\SeparateTimezoneCaster;
+use FireflyIII\Handlers\Observer\PiggyBankEventObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
+#[ObservedBy([PiggyBankEventObserver::class])]
class PiggyBankEvent extends Model
{
use ReturnsIntegerIdTrait;
@@ -47,7 +50,7 @@ class PiggyBankEvent extends Model
*/
public function setAmountAttribute($value): void
{
- $this->attributes['amount'] = (string) $value;
+ $this->attributes['amount'] = (string)$value;
}
public function transactionJournal(): BelongsTo
@@ -61,14 +64,7 @@ class PiggyBankEvent extends Model
protected function amount(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (string) $value,
- );
- }
-
- protected function piggyBankId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (string)$value,
);
}
@@ -82,4 +78,11 @@ class PiggyBankEvent extends Model
'native_amount' => 'string',
];
}
+
+ protected function piggyBankId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/PiggyBankRepetition.php b/app/Models/PiggyBankRepetition.php
index f0f6125b53..41af799266 100644
--- a/app/Models/PiggyBankRepetition.php
+++ b/app/Models/PiggyBankRepetition.php
@@ -23,10 +23,10 @@ declare(strict_types=1);
namespace FireflyIII\Models;
-use Illuminate\Database\Eloquent\Attributes\Scope;
use Carbon\Carbon;
use FireflyIII\Casts\SeparateTimezoneCaster;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
+use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
@@ -43,12 +43,48 @@ class PiggyBankRepetition extends Model
return $this->belongsTo(PiggyBank::class);
}
+ /**
+ * @param mixed $value
+ */
+ public function setCurrentAmountAttribute($value): void
+ {
+ $this->attributes['current_amount'] = (string)$value;
+ }
+
+ protected function casts(): array
+ {
+ return [
+ 'created_at' => 'datetime',
+ 'updated_at' => 'datetime',
+ 'start_date' => SeparateTimezoneCaster::class,
+ 'target_date' => SeparateTimezoneCaster::class,
+ 'virtual_balance' => 'string',
+ ];
+ }
+
+ /**
+ * Get the amount
+ */
+ protected function currentAmount(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (string)$value,
+ );
+ }
+
#[Scope]
protected function onDates(EloquentBuilder $query, Carbon $start, Carbon $target): EloquentBuilder
{
return $query->where('start_date', $start->format('Y-m-d'))->where('target_date', $target->format('Y-m-d'));
}
+ protected function piggyBankId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
/**
* @return EloquentBuilder
*/
@@ -69,40 +105,4 @@ class PiggyBankRepetition extends Model
)
;
}
-
- /**
- * @param mixed $value
- */
- public function setCurrentAmountAttribute($value): void
- {
- $this->attributes['current_amount'] = (string) $value;
- }
-
- /**
- * Get the amount
- */
- protected function currentAmount(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (string) $value,
- );
- }
-
- protected function piggyBankId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- protected function casts(): array
- {
- return [
- 'created_at' => 'datetime',
- 'updated_at' => 'datetime',
- 'start_date' => SeparateTimezoneCaster::class,
- 'target_date' => SeparateTimezoneCaster::class,
- 'virtual_balance' => 'string',
- ];
- }
}
diff --git a/app/Models/Preference.php b/app/Models/Preference.php
index 0e282cf1a5..151e5bbd35 100644
--- a/app/Models/Preference.php
+++ b/app/Models/Preference.php
@@ -50,7 +50,7 @@ class Preference extends Model
// some preferences do not have an administration ID.
// some need it, to make sure the correct one is selected.
- $userGroupId = (int) $user->user_group_id;
+ $userGroupId = (int)$user->user_group_id;
$userGroupId = 0 === $userGroupId ? null : $userGroupId;
/** @var null|Preference $preference */
@@ -67,7 +67,7 @@ class Preference extends Model
// try again with ID, but this time don't care about the preferred user_group_id
if (null === $preference) {
- $preference = $user->preferences()->where('id', (int) $value)->first();
+ $preference = $user->preferences()->where('id', (int)$value)->first();
}
if (null !== $preference) {
/** @var Preference $preference */
@@ -78,7 +78,7 @@ class Preference extends Model
$preference = new self();
$preference->name = $value;
$preference->data = $default[$value];
- $preference->user_id = (int) $user->id;
+ $preference->user_id = (int)$user->id;
$preference->user_group_id = in_array($value, $items, true) ? $userGroupId : null;
$preference->save();
diff --git a/app/Models/Recurrence.php b/app/Models/Recurrence.php
index 4e369fca27..b3fa7a3a8c 100644
--- a/app/Models/Recurrence.php
+++ b/app/Models/Recurrence.php
@@ -25,9 +25,11 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Casts\SeparateTimezoneCaster;
+use FireflyIII\Handlers\Observer\RecurrenceObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -36,6 +38,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+#[ObservedBy([RecurrenceObserver::class])]
class Recurrence extends Model
{
use ReturnsIntegerIdTrait;
@@ -55,7 +58,7 @@ class Recurrence extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $recurrenceId = (int) $value;
+ $recurrenceId = (int)$value;
/** @var User $user */
$user = auth()->user();
@@ -113,13 +116,6 @@ class Recurrence extends Model
return $this->belongsTo(TransactionType::class);
}
- protected function transactionTypeId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
protected function casts(): array
{
return [
@@ -139,4 +135,11 @@ class Recurrence extends Model
'user_group_id' => 'integer',
];
}
+
+ protected function transactionTypeId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/RecurrenceMeta.php b/app/Models/RecurrenceMeta.php
index 7430c942f6..d031e0b883 100644
--- a/app/Models/RecurrenceMeta.php
+++ b/app/Models/RecurrenceMeta.php
@@ -44,13 +44,6 @@ class RecurrenceMeta extends Model
return $this->belongsTo(Recurrence::class);
}
- protected function recurrenceId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
protected function casts(): array
{
return [
@@ -61,4 +54,11 @@ class RecurrenceMeta extends Model
'value' => 'string',
];
}
+
+ protected function recurrenceId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/RecurrenceRepetition.php b/app/Models/RecurrenceRepetition.php
index 93ad02a196..52d8259877 100644
--- a/app/Models/RecurrenceRepetition.php
+++ b/app/Models/RecurrenceRepetition.php
@@ -36,16 +36,20 @@ class RecurrenceRepetition extends Model
use ReturnsIntegerIdTrait;
use SoftDeletes;
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const int WEEKEND_DO_NOTHING = 1;
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const int WEEKEND_SKIP_CREATION = 2;
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const int WEEKEND_TO_FRIDAY = 3;
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const int WEEKEND_TO_MONDAY = 4;
protected $casts
@@ -78,21 +82,21 @@ class RecurrenceRepetition extends Model
protected function recurrenceId(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (int)$value,
);
}
protected function repetitionSkip(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (int)$value,
);
}
protected function weekend(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (int)$value,
);
}
}
diff --git a/app/Models/RecurrenceTransaction.php b/app/Models/RecurrenceTransaction.php
index 2c17d52ebb..c2d91a54ad 100644
--- a/app/Models/RecurrenceTransaction.php
+++ b/app/Models/RecurrenceTransaction.php
@@ -24,13 +24,16 @@ declare(strict_types=1);
namespace FireflyIII\Models;
+use FireflyIII\Handlers\Observer\RecurrenceTransactionObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
+#[ObservedBy([RecurrenceTransactionObserver::class])]
class RecurrenceTransaction extends Model
{
use ReturnsIntegerIdTrait;
@@ -88,49 +91,7 @@ class RecurrenceTransaction extends Model
protected function amount(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (string) $value,
- );
- }
-
- protected function destinationId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- protected function foreignAmount(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (string) $value,
- );
- }
-
- protected function recurrenceId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- protected function sourceId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- protected function transactionCurrencyId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- protected function userId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (string)$value,
);
}
@@ -145,4 +106,46 @@ class RecurrenceTransaction extends Model
'description' => 'string',
];
}
+
+ protected function destinationId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
+ protected function foreignAmount(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (string)$value,
+ );
+ }
+
+ protected function recurrenceId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
+ protected function sourceId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
+ protected function transactionCurrencyId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
+ protected function userId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/RecurrenceTransactionMeta.php b/app/Models/RecurrenceTransactionMeta.php
index a334b9c433..442da02559 100644
--- a/app/Models/RecurrenceTransactionMeta.php
+++ b/app/Models/RecurrenceTransactionMeta.php
@@ -44,13 +44,6 @@ class RecurrenceTransactionMeta extends Model
return $this->belongsTo(RecurrenceTransaction::class, 'rt_id');
}
- protected function rtId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
protected function casts(): array
{
return [
@@ -61,4 +54,11 @@ class RecurrenceTransactionMeta extends Model
'value' => 'string',
];
}
+
+ protected function rtId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/Rule.php b/app/Models/Rule.php
index 5308eae4d5..7755e00984 100644
--- a/app/Models/Rule.php
+++ b/app/Models/Rule.php
@@ -23,9 +23,11 @@ declare(strict_types=1);
namespace FireflyIII\Models;
+use FireflyIII\Handlers\Observer\RuleObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -33,6 +35,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+#[ObservedBy([RuleObserver::class])]
class Rule extends Model
{
use ReturnsIntegerIdTrait;
@@ -49,7 +52,7 @@ class Rule extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $ruleId = (int) $value;
+ $ruleId = (int)$value;
/** @var User $user */
$user = auth()->user();
@@ -84,30 +87,11 @@ class Rule extends Model
return $this->hasMany(RuleTrigger::class);
}
- protected function description(): Attribute
- {
- return Attribute::make(set: fn ($value) => ['description' => e($value)]);
- }
-
public function userGroup(): BelongsTo
{
return $this->belongsTo(UserGroup::class);
}
- protected function order(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- protected function ruleGroupId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
protected function casts(): array
{
return [
@@ -123,4 +107,23 @@ class Rule extends Model
'user_group_id' => 'integer',
];
}
+
+ protected function description(): Attribute
+ {
+ return Attribute::make(set: fn ($value) => ['description' => e($value)]);
+ }
+
+ protected function order(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
+ protected function ruleGroupId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/RuleAction.php b/app/Models/RuleAction.php
index 60ebc7254f..0bb29a5236 100644
--- a/app/Models/RuleAction.php
+++ b/app/Models/RuleAction.php
@@ -42,7 +42,7 @@ class RuleAction extends Model
if (false === config('firefly.feature_flags.expression_engine')) {
Log::debug('Expression engine is disabled, returning action value as string.');
- return (string) $this->action_value;
+ return (string)$this->action_value;
}
if (true === config('firefly.feature_flags.expression_engine') && str_starts_with($this->action_value, '\=')) {
// return literal string.
@@ -54,7 +54,7 @@ class RuleAction extends Model
$result = $expr->evaluate($journal);
} catch (SyntaxError $e) {
Log::error(sprintf('Expression engine failed to evaluate expression "%s" with error "%s".', $this->action_value, $e->getMessage()));
- $result = (string) $this->action_value;
+ $result = (string)$this->action_value;
}
Log::debug(sprintf('Expression engine is enabled, result of expression "%s" is "%s".', $this->action_value, $result));
@@ -66,20 +66,6 @@ class RuleAction extends Model
return $this->belongsTo(Rule::class);
}
- protected function order(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- protected function ruleId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
protected function casts(): array
{
return [
@@ -90,4 +76,18 @@ class RuleAction extends Model
'stop_processing' => 'boolean',
];
}
+
+ protected function order(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
+ protected function ruleId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/RuleGroup.php b/app/Models/RuleGroup.php
index 1a25031a7e..99f6655f63 100644
--- a/app/Models/RuleGroup.php
+++ b/app/Models/RuleGroup.php
@@ -23,9 +23,11 @@ declare(strict_types=1);
namespace FireflyIII\Models;
+use FireflyIII\Handlers\Observer\RuleGroupObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -33,6 +35,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+#[ObservedBy([RuleGroupObserver::class])]
class RuleGroup extends Model
{
use ReturnsIntegerIdTrait;
@@ -49,7 +52,7 @@ class RuleGroup extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $ruleGroupId = (int) $value;
+ $ruleGroupId = (int)$value;
/** @var User $user */
$user = auth()->user();
@@ -74,13 +77,6 @@ class RuleGroup extends Model
return $this->hasMany(Rule::class);
}
- protected function order(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
protected function casts(): array
{
return [
@@ -94,4 +90,11 @@ class RuleGroup extends Model
'user_group_id' => 'integer',
];
}
+
+ protected function order(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/RuleTrigger.php b/app/Models/RuleTrigger.php
index 5c9f4fca2e..c3de8048ba 100644
--- a/app/Models/RuleTrigger.php
+++ b/app/Models/RuleTrigger.php
@@ -39,20 +39,6 @@ class RuleTrigger extends Model
return $this->belongsTo(Rule::class);
}
- protected function order(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- protected function ruleId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
protected function casts(): array
{
return [
@@ -63,4 +49,18 @@ class RuleTrigger extends Model
'stop_processing' => 'boolean',
];
}
+
+ protected function order(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
+ protected function ruleId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/Tag.php b/app/Models/Tag.php
index 3af4799730..f348bea48a 100644
--- a/app/Models/Tag.php
+++ b/app/Models/Tag.php
@@ -24,9 +24,11 @@ declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\Casts\SeparateTimezoneCaster;
+use FireflyIII\Handlers\Observer\TagObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@@ -34,6 +36,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+#[ObservedBy([TagObserver::class])]
class Tag extends Model
{
use ReturnsIntegerIdTrait;
@@ -42,7 +45,7 @@ class Tag extends Model
protected $fillable = ['user_id', 'user_group_id', 'tag', 'date', 'date_tz', 'description', 'tag_mode'];
- protected $hidden = ['zoomLevel', 'latitude', 'longitude'];
+ protected $hidden = ['zoomLevel', 'zoom_level', 'latitude', 'longitude'];
/**
* Route binder. Converts the key in the URL to the specified object (or throw 404).
@@ -52,7 +55,7 @@ class Tag extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $tagId = (int) $value;
+ $tagId = (int)$value;
/** @var User $user */
$user = auth()->user();
@@ -101,4 +104,9 @@ class Tag extends Model
'user_group_id' => 'integer',
];
}
+
+ public function primaryPeriodStatistics(): MorphMany
+ {
+ return $this->morphMany(PeriodStatistic::class, 'primary_statable');
+ }
}
diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php
index a563e3656f..e33cb0a1ac 100644
--- a/app/Models/Transaction.php
+++ b/app/Models/Transaction.php
@@ -23,9 +23,11 @@ declare(strict_types=1);
namespace FireflyIII\Models;
-use Illuminate\Database\Eloquent\Attributes\Scope;
use Carbon\Carbon;
+use FireflyIII\Handlers\Observer\TransactionObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
+use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -34,6 +36,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
+#[ObservedBy([TransactionObserver::class])]
class Transaction extends Model
{
use HasFactory;
@@ -89,6 +92,31 @@ class Transaction extends Model
return $this->belongsTo(TransactionCurrency::class, 'foreign_currency_id');
}
+ /**
+ * @param mixed $value
+ */
+ public function setAmountAttribute($value): void
+ {
+ $this->attributes['amount'] = (string)$value;
+ }
+
+ public function transactionCurrency(): BelongsTo
+ {
+ return $this->belongsTo(TransactionCurrency::class);
+ }
+
+ public function transactionJournal(): BelongsTo
+ {
+ return $this->belongsTo(TransactionJournal::class);
+ }
+
+ protected function accountId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
/**
* Check for transactions AFTER a specified date.
*/
@@ -117,6 +145,23 @@ class Transaction extends Model
return false;
}
+ /**
+ * Get the amount
+ */
+ protected function amount(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (string)$value,
+ );
+ }
+
+ protected function balanceDirty(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => 1 === (int)$value,
+ );
+ }
+
/**
* Check for transactions BEFORE the specified date.
*/
@@ -129,78 +174,6 @@ class Transaction extends Model
$query->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59'));
}
- #[Scope]
- protected function transactionTypes(Builder $query, array $types): void
- {
- if (!self::isJoined($query, 'transaction_journals')) {
- $query->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id');
- }
-
- if (!self::isJoined($query, 'transaction_types')) {
- $query->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id');
- }
- $query->whereIn('transaction_types.type', $types);
- }
-
- /**
- * @param mixed $value
- */
- public function setAmountAttribute($value): void
- {
- $this->attributes['amount'] = (string) $value;
- }
-
- public function transactionCurrency(): BelongsTo
- {
- return $this->belongsTo(TransactionCurrency::class);
- }
-
- public function transactionJournal(): BelongsTo
- {
- return $this->belongsTo(TransactionJournal::class);
- }
-
- protected function accountId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- /**
- * Get the amount
- */
- protected function amount(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (string) $value,
- );
- }
-
- protected function balanceDirty(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => 1 === (int) $value,
- );
- }
-
- /**
- * Get the foreign amount
- */
- protected function foreignAmount(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (string) $value,
- );
- }
-
- protected function transactionJournalId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
protected function casts(): array
{
return [
@@ -221,4 +194,34 @@ class Transaction extends Model
'native_foreign_amount' => 'string',
];
}
+
+ /**
+ * Get the foreign amount
+ */
+ protected function foreignAmount(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (string)$value,
+ );
+ }
+
+ protected function transactionJournalId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
+ #[Scope]
+ protected function transactionTypes(Builder $query, array $types): void
+ {
+ if (!self::isJoined($query, 'transaction_journals')) {
+ $query->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id');
+ }
+
+ if (!self::isJoined($query, 'transaction_types')) {
+ $query->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id');
+ }
+ $query->whereIn('transaction_types.type', $types);
+ }
}
diff --git a/app/Models/TransactionCurrency.php b/app/Models/TransactionCurrency.php
index 83dfd6bfe0..6f02375355 100644
--- a/app/Models/TransactionCurrency.php
+++ b/app/Models/TransactionCurrency.php
@@ -50,7 +50,7 @@ class TransactionCurrency extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $currencyId = (int) $value;
+ $currencyId = (int)$value;
$currency = self::find($currencyId);
if (null !== $currency) {
$currency->refreshForUser(auth()->user());
@@ -101,13 +101,6 @@ class TransactionCurrency extends Model
return $this->belongsToMany(User::class)->withTimestamps()->withPivot('user_default');
}
- protected function decimalPlaces(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
protected function casts(): array
{
return [
@@ -118,4 +111,11 @@ class TransactionCurrency extends Model
'enabled' => 'bool',
];
}
+
+ protected function decimalPlaces(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/TransactionGroup.php b/app/Models/TransactionGroup.php
index a09af2b231..81e7aac2e6 100644
--- a/app/Models/TransactionGroup.php
+++ b/app/Models/TransactionGroup.php
@@ -23,15 +23,18 @@ declare(strict_types=1);
namespace FireflyIII\Models;
+use FireflyIII\Handlers\Observer\TransactionGroupObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+#[ObservedBy([TransactionGroupObserver::class])]
class TransactionGroup extends Model
{
use ReturnsIntegerIdTrait;
@@ -49,7 +52,7 @@ class TransactionGroup extends Model
{
app('log')->debug(sprintf('Now in %s("%s")', __METHOD__, $value));
if (auth()->check()) {
- $groupId = (int) $value;
+ $groupId = (int)$value;
/** @var User $user */
$user = auth()->user();
diff --git a/app/Models/TransactionJournal.php b/app/Models/TransactionJournal.php
index 609d7dc353..3aa0099446 100644
--- a/app/Models/TransactionJournal.php
+++ b/app/Models/TransactionJournal.php
@@ -23,13 +23,15 @@ declare(strict_types=1);
namespace FireflyIII\Models;
-use Illuminate\Database\Eloquent\Attributes\Scope;
use Carbon\Carbon;
use FireflyIII\Casts\SeparateTimezoneCaster;
use FireflyIII\Enums\TransactionTypeEnum;
+use FireflyIII\Handlers\Observer\TransactionJournalObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
+use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -46,6 +48,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
* @method EloquentBuilder|static after()
* @method static EloquentBuilder|static query()
*/
+#[ObservedBy([TransactionJournalObserver::class])]
class TransactionJournal extends Model
{
use HasFactory;
@@ -78,7 +81,7 @@ class TransactionJournal extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $journalId = (int) $value;
+ $journalId = (int)$value;
/** @var User $user */
$user = auth()->user();
@@ -165,32 +168,6 @@ class TransactionJournal extends Model
return $query->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s'));
}
- #[Scope]
- protected function transactionTypes(EloquentBuilder $query, array $types): void
- {
- if (!self::isJoined($query, 'transaction_types')) {
- $query->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id');
- }
- if (0 !== count($types)) {
- $query->whereIn('transaction_types.type', $types);
- }
- }
-
- /**
- * Checks if tables are joined.
- */
- public static function isJoined(EloquentBuilder $query, string $table): bool
- {
- $joins = $query->getQuery()->joins;
- foreach ($joins as $join) {
- if ($join->table === $table) {
- return true;
- }
- }
-
- return false;
- }
-
public function sourceJournalLinks(): HasMany
{
return $this->hasMany(TransactionJournalLink::class, 'source_id');
@@ -231,20 +208,6 @@ class TransactionJournal extends Model
return $this->belongsTo(UserGroup::class);
}
- protected function order(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- protected function transactionTypeId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
protected function casts(): array
{
return [
@@ -263,4 +226,44 @@ class TransactionJournal extends Model
'user_group_id' => 'integer',
];
}
+
+ protected function order(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
+ protected function transactionTypeId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
+ #[Scope]
+ protected function transactionTypes(EloquentBuilder $query, array $types): void
+ {
+ if (!self::isJoined($query, 'transaction_types')) {
+ $query->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id');
+ }
+ if (0 !== count($types)) {
+ $query->whereIn('transaction_types.type', $types);
+ }
+ }
+
+ /**
+ * Checks if tables are joined.
+ */
+ public static function isJoined(EloquentBuilder $query, string $table): bool
+ {
+ $joins = $query->getQuery()->joins;
+ foreach ($joins as $join) {
+ if ($join->table === $table) {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
diff --git a/app/Models/TransactionJournalLink.php b/app/Models/TransactionJournalLink.php
index 92adb6a7e2..ab1a40c260 100644
--- a/app/Models/TransactionJournalLink.php
+++ b/app/Models/TransactionJournalLink.php
@@ -44,7 +44,7 @@ class TransactionJournalLink extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $linkId = (int) $value;
+ $linkId = (int)$value;
$link = self::where('journal_links.id', $linkId)
->leftJoin('transaction_journals as t_a', 't_a.id', '=', 'source_id')
->leftJoin('transaction_journals as t_b', 't_b.id', '=', 'destination_id')
@@ -83,27 +83,6 @@ class TransactionJournalLink extends Model
return $this->belongsTo(TransactionJournal::class, 'source_id');
}
- protected function destinationId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- protected function linkTypeId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
- protected function sourceId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
protected function casts(): array
{
return [
@@ -111,4 +90,25 @@ class TransactionJournalLink extends Model
'updated_at' => 'datetime',
];
}
+
+ protected function destinationId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
+ protected function linkTypeId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
+
+ protected function sourceId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/TransactionJournalMeta.php b/app/Models/TransactionJournalMeta.php
index 80b7e42ca0..4a748bfb8b 100644
--- a/app/Models/TransactionJournalMeta.php
+++ b/app/Models/TransactionJournalMeta.php
@@ -41,27 +41,11 @@ class TransactionJournalMeta extends Model
protected $table = 'journal_meta';
- protected function data(): Attribute
- {
- return Attribute::make(get: fn ($value) => json_decode((string) $value, false), set: function ($value) {
- $data = json_encode($value);
-
- return ['data' => $data, 'hash' => hash('sha256', $data)];
- });
- }
-
public function transactionJournal(): BelongsTo
{
return $this->belongsTo(TransactionJournal::class);
}
- protected function transactionJournalId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
protected function casts(): array
{
return [
@@ -70,4 +54,20 @@ class TransactionJournalMeta extends Model
'deleted_at' => 'datetime',
];
}
+
+ protected function data(): Attribute
+ {
+ return Attribute::make(get: fn ($value) => json_decode((string)$value, false), set: function ($value) {
+ $data = json_encode($value);
+
+ return ['data' => $data, 'hash' => hash('sha256', $data)];
+ });
+ }
+
+ protected function transactionJournalId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/TransactionType.php b/app/Models/TransactionType.php
index 0b04ead8b5..cac754f110 100644
--- a/app/Models/TransactionType.php
+++ b/app/Models/TransactionType.php
@@ -36,25 +36,32 @@ class TransactionType extends Model
use ReturnsIntegerIdTrait;
use SoftDeletes;
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string DEPOSIT = 'Deposit';
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string INVALID = 'Invalid';
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string LIABILITY_CREDIT = 'Liability credit';
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string OPENING_BALANCE = 'Opening balance';
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string RECONCILIATION = 'Reconciliation';
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string TRANSFER = 'Transfer';
- #[Deprecated] /** @deprecated */
+ #[Deprecated]
+ /** @deprecated */
public const string WITHDRAWAL = 'Withdrawal';
protected $casts
diff --git a/app/Models/UserGroup.php b/app/Models/UserGroup.php
index fbae2d70f6..e0ff63268a 100644
--- a/app/Models/UserGroup.php
+++ b/app/Models/UserGroup.php
@@ -47,7 +47,7 @@ class UserGroup extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $userGroupId = (int) $value;
+ $userGroupId = (int)$value;
/** @var User $user */
$user = auth()->user();
@@ -76,6 +76,14 @@ class UserGroup extends Model
return $this->hasMany(Account::class);
}
+ /**
+ * Link to accounts.
+ */
+ public function periodStatistics(): HasMany
+ {
+ return $this->hasMany(PeriodStatistic::class);
+ }
+
/**
* Link to attachments.
*/
diff --git a/app/Models/Webhook.php b/app/Models/Webhook.php
index a836b7fad6..ab7e477e37 100644
--- a/app/Models/Webhook.php
+++ b/app/Models/Webhook.php
@@ -27,9 +27,11 @@ namespace FireflyIII\Models;
use FireflyIII\Enums\WebhookDelivery as WebhookDeliveryEnum;
use FireflyIII\Enums\WebhookResponse as WebhookResponseEnum;
use FireflyIII\Enums\WebhookTrigger as WebhookTriggerEnum;
+use FireflyIII\Handlers\Observer\WebhookObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait;
use FireflyIII\User;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@@ -37,6 +39,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+#[ObservedBy([WebhookObserver::class])]
class Webhook extends Model
{
use ReturnsIntegerIdTrait;
@@ -151,16 +154,16 @@ class Webhook extends Model
return $this->belongsTo(User::class);
}
- public function webhookMessages(): HasMany
- {
- return $this->hasMany(WebhookMessage::class);
- }
-
public function webhookDeliveries(): BelongsToMany
{
return $this->belongsToMany(WebhookDelivery::class);
}
+ public function webhookMessages(): HasMany
+ {
+ return $this->hasMany(WebhookMessage::class);
+ }
+
public function webhookResponses(): BelongsToMany
{
return $this->belongsToMany(WebhookResponse::class);
diff --git a/app/Models/WebhookAttempt.php b/app/Models/WebhookAttempt.php
index fa283f9ca2..a2de5dac3b 100644
--- a/app/Models/WebhookAttempt.php
+++ b/app/Models/WebhookAttempt.php
@@ -45,7 +45,7 @@ class WebhookAttempt extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $attemptId = (int) $value;
+ $attemptId = (int)$value;
/** @var User $user */
$user = auth()->user();
@@ -68,7 +68,7 @@ class WebhookAttempt extends Model
protected function webhookMessageId(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (int)$value,
);
}
}
diff --git a/app/Models/WebhookDelivery.php b/app/Models/WebhookDelivery.php
index a43a47d417..33acf3b9b7 100644
--- a/app/Models/WebhookDelivery.php
+++ b/app/Models/WebhookDelivery.php
@@ -41,7 +41,7 @@ class WebhookDelivery extends Model
protected function key(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (int)$value,
);
}
}
diff --git a/app/Models/WebhookMessage.php b/app/Models/WebhookMessage.php
index 77d6a0642c..dbccb97cbd 100644
--- a/app/Models/WebhookMessage.php
+++ b/app/Models/WebhookMessage.php
@@ -24,14 +24,17 @@ declare(strict_types=1);
namespace FireflyIII\Models;
+use FireflyIII\Handlers\Observer\WebhookMessageObserver;
use FireflyIII\Support\Models\ReturnsIntegerIdTrait;
use FireflyIII\User;
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+#[ObservedBy([WebhookMessageObserver::class])]
class WebhookMessage extends Model
{
use ReturnsIntegerIdTrait;
@@ -44,7 +47,7 @@ class WebhookMessage extends Model
public static function routeBinder(string $value): self
{
if (auth()->check()) {
- $messageId = (int) $value;
+ $messageId = (int)$value;
/** @var User $user */
$user = auth()->user();
@@ -69,23 +72,6 @@ class WebhookMessage extends Model
return $this->hasMany(WebhookAttempt::class);
}
- /**
- * Get the amount
- */
- protected function sent(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (bool) $value,
- );
- }
-
- protected function webhookId(): Attribute
- {
- return Attribute::make(
- get: static fn ($value) => (int) $value,
- );
- }
-
protected function casts(): array
{
return [
@@ -96,4 +82,21 @@ class WebhookMessage extends Model
'logs' => 'json',
];
}
+
+ /**
+ * Get the amount
+ */
+ protected function sent(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (bool)$value,
+ );
+ }
+
+ protected function webhookId(): Attribute
+ {
+ return Attribute::make(
+ get: static fn ($value) => (int)$value,
+ );
+ }
}
diff --git a/app/Models/WebhookResponse.php b/app/Models/WebhookResponse.php
index c970e8b70e..7b3e785a73 100644
--- a/app/Models/WebhookResponse.php
+++ b/app/Models/WebhookResponse.php
@@ -41,7 +41,7 @@ class WebhookResponse extends Model
protected function key(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (int)$value,
);
}
}
diff --git a/app/Models/WebhookTrigger.php b/app/Models/WebhookTrigger.php
index 4bd8cf444d..b7ccd7cfc5 100644
--- a/app/Models/WebhookTrigger.php
+++ b/app/Models/WebhookTrigger.php
@@ -41,7 +41,7 @@ class WebhookTrigger extends Model
protected function key(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (int)$value,
);
}
}
diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php
index 5cff75ae42..1b221013f5 100644
--- a/app/Providers/EventServiceProvider.php
+++ b/app/Providers/EventServiceProvider.php
@@ -57,46 +57,6 @@ use FireflyIII\Events\TriggeredAuditLog;
use FireflyIII\Events\UpdatedAccount;
use FireflyIII\Events\UpdatedTransactionGroup;
use FireflyIII\Events\UserChangedEmail;
-use FireflyIII\Handlers\Observer\AccountObserver;
-use FireflyIII\Handlers\Observer\AttachmentObserver;
-use FireflyIII\Handlers\Observer\AutoBudgetObserver;
-use FireflyIII\Handlers\Observer\AvailableBudgetObserver;
-use FireflyIII\Handlers\Observer\BillObserver;
-use FireflyIII\Handlers\Observer\BudgetLimitObserver;
-use FireflyIII\Handlers\Observer\BudgetObserver;
-use FireflyIII\Handlers\Observer\CategoryObserver;
-use FireflyIII\Handlers\Observer\PiggyBankEventObserver;
-use FireflyIII\Handlers\Observer\PiggyBankObserver;
-use FireflyIII\Handlers\Observer\RecurrenceObserver;
-use FireflyIII\Handlers\Observer\RecurrenceTransactionObserver;
-use FireflyIII\Handlers\Observer\RuleGroupObserver;
-use FireflyIII\Handlers\Observer\RuleObserver;
-use FireflyIII\Handlers\Observer\TagObserver;
-use FireflyIII\Handlers\Observer\TransactionGroupObserver;
-use FireflyIII\Handlers\Observer\TransactionJournalObserver;
-use FireflyIII\Handlers\Observer\TransactionObserver;
-use FireflyIII\Handlers\Observer\WebhookMessageObserver;
-use FireflyIII\Handlers\Observer\WebhookObserver;
-use FireflyIII\Models\Account;
-use FireflyIII\Models\Attachment;
-use FireflyIII\Models\AutoBudget;
-use FireflyIII\Models\AvailableBudget;
-use FireflyIII\Models\Bill;
-use FireflyIII\Models\Budget;
-use FireflyIII\Models\BudgetLimit;
-use FireflyIII\Models\Category;
-use FireflyIII\Models\PiggyBank;
-use FireflyIII\Models\PiggyBankEvent;
-use FireflyIII\Models\Recurrence;
-use FireflyIII\Models\RecurrenceTransaction;
-use FireflyIII\Models\Rule;
-use FireflyIII\Models\RuleGroup;
-use FireflyIII\Models\Tag;
-use FireflyIII\Models\Transaction;
-use FireflyIII\Models\TransactionGroup;
-use FireflyIII\Models\TransactionJournal;
-use FireflyIII\Models\Webhook;
-use FireflyIII\Models\WebhookMessage;
use Illuminate\Auth\Events\Login;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Laravel\Passport\Events\AccessTokenCreated;
@@ -256,32 +216,5 @@ class EventServiceProvider extends ServiceProvider
* Register any events for your application.
*/
#[Override]
- public function boot(): void
- {
- $this->registerObservers();
- }
-
- private function registerObservers(): void
- {
- Attachment::observe(new AttachmentObserver());
- Account::observe(new AccountObserver());
- AutoBudget::observe(new AutoBudgetObserver());
- AvailableBudget::observe(new AvailableBudgetObserver());
- Bill::observe(new BillObserver());
- Budget::observe(new BudgetObserver());
- BudgetLimit::observe(new BudgetLimitObserver());
- Category::observe(new CategoryObserver());
- PiggyBank::observe(new PiggyBankObserver());
- PiggyBankEvent::observe(new PiggyBankEventObserver());
- Recurrence::observe(new RecurrenceObserver());
- RecurrenceTransaction::observe(new RecurrenceTransactionObserver());
- Rule::observe(new RuleObserver());
- RuleGroup::observe(new RuleGroupObserver());
- Tag::observe(new TagObserver());
- Transaction::observe(new TransactionObserver());
- TransactionJournal::observe(new TransactionJournalObserver());
- TransactionGroup::observe(new TransactionGroupObserver());
- Webhook::observe(new WebhookObserver());
- WebhookMessage::observe(new WebhookMessageObserver());
- }
+ public function boot(): void {}
}
diff --git a/app/Providers/FireflyServiceProvider.php b/app/Providers/FireflyServiceProvider.php
index 02ff3d729b..e900b72f9e 100644
--- a/app/Providers/FireflyServiceProvider.php
+++ b/app/Providers/FireflyServiceProvider.php
@@ -43,6 +43,8 @@ use FireflyIII\Repositories\AuditLogEntry\ALERepository;
use FireflyIII\Repositories\AuditLogEntry\ALERepositoryInterface;
use FireflyIII\Repositories\ObjectGroup\ObjectGroupRepository;
use FireflyIII\Repositories\ObjectGroup\ObjectGroupRepositoryInterface;
+use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepository;
+use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface;
use FireflyIII\Repositories\TransactionType\TransactionTypeRepository;
use FireflyIII\Repositories\TransactionType\TransactionTypeRepositoryInterface;
use FireflyIII\Repositories\User\UserRepository;
@@ -174,6 +176,18 @@ class FireflyServiceProvider extends ServiceProvider
}
);
+ $this->app->bind(
+ static function (Application $app): PeriodStatisticRepositoryInterface {
+ /** @var PeriodStatisticRepository $repository */
+ $repository = app(PeriodStatisticRepository::class);
+ if ($app->auth->check()) { // @phpstan-ignore-line (phpstan does not understand the reference to auth)
+ $repository->setUser(auth()->user());
+ }
+
+ return $repository;
+ }
+ );
+
$this->app->bind(
static function (Application $app): WebhookRepositoryInterface {
/** @var WebhookRepository $repository */
diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php
index c9039b7c0f..8eacb438b4 100644
--- a/app/Repositories/Account/AccountRepository.php
+++ b/app/Repositories/Account/AccountRepository.php
@@ -45,6 +45,7 @@ use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Override;
@@ -150,18 +151,18 @@ class AccountRepository implements AccountRepositoryInterface, UserGroupInterfac
$query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
$query->whereIn('account_types.type', $types);
}
- app('log')->debug(sprintf('Searching for account named "%s" (of user #%d) of the following type(s)', $name, $this->user->id), ['types' => $types]);
+ Log::debug(sprintf('Searching for account named "%s" (of user #%d) of the following type(s)', $name, $this->user->id), ['types' => $types]);
$query->where('accounts.name', $name);
/** @var null|Account $account */
$account = $query->first(['accounts.*']);
if (null === $account) {
- app('log')->debug(sprintf('There is no account with name "%s" of types', $name), $types);
+ Log::debug(sprintf('There is no account with name "%s" of types', $name), $types);
return null;
}
- app('log')->debug(sprintf('Found #%d (%s) with type id %d', $account->id, $account->name, $account->account_type_id));
+ Log::debug(sprintf('Found #%d (%s) with type id %d', $account->id, $account->name, $account->account_type_id));
return $account;
}
@@ -465,14 +466,14 @@ class AccountRepository implements AccountRepositoryInterface, UserGroupInterfac
];
if (array_key_exists(ucfirst($type), $sets)) {
$order = (int) $this->getAccountsByType($sets[ucfirst($type)])->max('order');
- app('log')->debug(sprintf('Return max order of "%s" set: %d', $type, $order));
+ Log::debug(sprintf('Return max order of "%s" set: %d', $type, $order));
return $order;
}
$specials = [AccountTypeEnum::CASH->value, AccountTypeEnum::INITIAL_BALANCE->value, AccountTypeEnum::IMPORT->value, AccountTypeEnum::RECONCILIATION->value];
$order = (int) $this->getAccountsByType($specials)->max('order');
- app('log')->debug(sprintf('Return max order of "%s" set (specials!): %d', $type, $order));
+ Log::debug(sprintf('Return max order of "%s" set (specials!): %d', $type, $order));
return $order;
}
@@ -545,6 +546,8 @@ class AccountRepository implements AccountRepositoryInterface, UserGroupInterfac
#[Override]
public function periodCollection(Account $account, Carbon $start, Carbon $end): array
{
+ Log::debug(sprintf('periodCollection(#%d, %s, %s)', $account->id, $start->format('Y-m-d'), $end->format('Y-m-d')));
+
return $account->transactions()
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
@@ -599,7 +602,7 @@ class AccountRepository implements AccountRepositoryInterface, UserGroupInterfac
continue;
}
if ($index !== (int) $account->order) {
- app('log')->debug(sprintf('Account #%d ("%s"): order should %d be but is %d.', $account->id, $account->name, $index, $account->order));
+ Log::debug(sprintf('Account #%d ("%s"): order should %d be but is %d.', $account->id, $account->name, $index, $account->order));
$account->order = $index;
$account->save();
}
diff --git a/app/Repositories/Budget/BudgetLimitRepository.php b/app/Repositories/Budget/BudgetLimitRepository.php
index 8974613364..ab80405609 100644
--- a/app/Repositories/Budget/BudgetLimitRepository.php
+++ b/app/Repositories/Budget/BudgetLimitRepository.php
@@ -31,8 +31,10 @@ use FireflyIII\Models\Budget;
use FireflyIII\Models\BudgetLimit;
use FireflyIII\Models\Note;
use FireflyIII\Models\TransactionCurrency;
+use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Repositories\UserGroup\UserGroupInterface;
use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait;
+use FireflyIII\Support\Singleton\PreferencesSingleton;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
@@ -271,7 +273,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
$factory = app(TransactionCurrencyFactory::class);
$currency = $factory->find($data['currency_id'] ?? null, $data['currency_code'] ?? null);
if (null === $currency) {
- $currency = app('amount')->getPrimaryCurrencyByUserGroup($this->user->userGroup);
+ $currency = Amount::getPrimaryCurrencyByUserGroup($this->user->userGroup);
}
$currency->enabled = true;
$currency->save();
@@ -293,7 +295,11 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
if (null !== $limit) {
throw new FireflyException('200027: Budget limit already exists.');
}
- app('log')->debug('No existing budget limit, create a new one');
+ Log::debug('No existing budget limit, create a new one');
+
+ // this is a lame trick to communicate with the observer.
+ $singleton = PreferencesSingleton::getInstance();
+ $singleton->setPreference('fire_webhooks_bl_store', $data['fire_webhooks'] ?? true);
// or create one and return it.
$limit = new BudgetLimit();
@@ -309,7 +315,7 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
$this->setNoteText($limit, $noteText);
}
- app('log')->debug(sprintf('Created new budget limit with ID #%d and amount %s', $limit->id, $data['amount']));
+ Log::debug(sprintf('Created new budget limit with ID #%d and amount %s', $limit->id, $data['amount']));
return $limit;
}
@@ -369,11 +375,15 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
}
// catch unexpected null:
if (null === $currency) {
- $currency = $budgetLimit->transactionCurrency ?? app('amount')->getPrimaryCurrencyByUserGroup($this->user->userGroup);
+ $currency = $budgetLimit->transactionCurrency ?? Amount::getPrimaryCurrencyByUserGroup($this->user->userGroup);
}
$currency->enabled = true;
$currency->save();
+ // this is a lame trick to communicate with the observer.
+ $singleton = PreferencesSingleton::getInstance();
+ $singleton->setPreference('fire_webhooks_bl_update', $data['fire_webhooks'] ?? true);
+
$budgetLimit->transaction_currency_id = $currency->id;
$budgetLimit->save();
@@ -385,63 +395,63 @@ class BudgetLimitRepository implements BudgetLimitRepositoryInterface, UserGroup
return $budgetLimit;
}
- public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, string $amount): ?BudgetLimit
- {
- // count the limits:
- $limits = $budget->budgetlimits()
- ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00'))
- ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00'))
- ->count('budget_limits.*')
- ;
- app('log')->debug(sprintf('Found %d budget limits.', $limits));
-
- // there might be a budget limit for these dates:
- /** @var null|BudgetLimit $limit */
- $limit = $budget->budgetlimits()
- ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00'))
- ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00'))
- ->first(['budget_limits.*'])
- ;
-
- // if more than 1 limit found, delete the others:
- if ($limits > 1 && null !== $limit) {
- app('log')->debug(sprintf('Found more than 1, delete all except #%d', $limit->id));
- $budget->budgetlimits()
- ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00'))
- ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00'))
- ->where('budget_limits.id', '!=', $limit->id)->delete()
- ;
- }
-
- // delete if amount is zero.
- // Returns 0 if the two operands are equal,
- // 1 if the left_operand is larger than the right_operand, -1 otherwise.
- if (null !== $limit && bccomp($amount, '0') <= 0) {
- app('log')->debug(sprintf('%s is zero, delete budget limit #%d', $amount, $limit->id));
- $limit->delete();
-
- return null;
- }
- // update if exists:
- if (null !== $limit) {
- app('log')->debug(sprintf('Existing budget limit is #%d, update this to amount %s', $limit->id, $amount));
- $limit->amount = $amount;
- $limit->save();
-
- return $limit;
- }
- app('log')->debug('No existing budget limit, create a new one');
- // or create one and return it.
- $limit = new BudgetLimit();
- $limit->budget()->associate($budget);
- $limit->start_date = $start->startOfDay();
- $limit->start_date_tz = $start->format('e');
- $limit->end_date = $end->startOfDay();
- $limit->end_date_tz = $end->format('e');
- $limit->amount = $amount;
- $limit->save();
- app('log')->debug(sprintf('Created new budget limit with ID #%d and amount %s', $limit->id, $amount));
-
- return $limit;
- }
+ // public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, string $amount): ?BudgetLimit
+ // {
+ // // count the limits:
+ // $limits = $budget->budgetlimits()
+ // ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00'))
+ // ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00'))
+ // ->count('budget_limits.*')
+ // ;
+ // Log::debug(sprintf('Found %d budget limits.', $limits));
+ //
+ // // there might be a budget limit for these dates:
+ // /** @var null|BudgetLimit $limit */
+ // $limit = $budget->budgetlimits()
+ // ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00'))
+ // ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00'))
+ // ->first(['budget_limits.*'])
+ // ;
+ //
+ // // if more than 1 limit found, delete the others:
+ // if ($limits > 1 && null !== $limit) {
+ // Log::debug(sprintf('Found more than 1, delete all except #%d', $limit->id));
+ // $budget->budgetlimits()
+ // ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00'))
+ // ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00'))
+ // ->where('budget_limits.id', '!=', $limit->id)->delete()
+ // ;
+ // }
+ //
+ // // delete if amount is zero.
+ // // Returns 0 if the two operands are equal,
+ // // 1 if the left_operand is larger than the right_operand, -1 otherwise.
+ // if (null !== $limit && bccomp($amount, '0') <= 0) {
+ // Log::debug(sprintf('%s is zero, delete budget limit #%d', $amount, $limit->id));
+ // $limit->delete();
+ //
+ // return null;
+ // }
+ // // update if exists:
+ // if (null !== $limit) {
+ // Log::debug(sprintf('Existing budget limit is #%d, update this to amount %s', $limit->id, $amount));
+ // $limit->amount = $amount;
+ // $limit->save();
+ //
+ // return $limit;
+ // }
+ // Log::debug('No existing budget limit, create a new one');
+ // // or create one and return it.
+ // $limit = new BudgetLimit();
+ // $limit->budget()->associate($budget);
+ // $limit->start_date = $start->startOfDay();
+ // $limit->start_date_tz = $start->format('e');
+ // $limit->end_date = $end->startOfDay();
+ // $limit->end_date_tz = $end->format('e');
+ // $limit->amount = $amount;
+ // $limit->save();
+ // Log::debug(sprintf('Created new budget limit with ID #%d and amount %s', $limit->id, $amount));
+ //
+ // return $limit;
+ // }
}
diff --git a/app/Repositories/Budget/BudgetLimitRepositoryInterface.php b/app/Repositories/Budget/BudgetLimitRepositoryInterface.php
index c56093ef61..defb1c7d49 100644
--- a/app/Repositories/Budget/BudgetLimitRepositoryInterface.php
+++ b/app/Repositories/Budget/BudgetLimitRepositoryInterface.php
@@ -81,5 +81,5 @@ interface BudgetLimitRepositoryInterface
public function update(BudgetLimit $budgetLimit, array $data): BudgetLimit;
- public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, string $amount): ?BudgetLimit;
+ // public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, string $amount): ?BudgetLimit;
}
diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php
index 3aa57328de..8aaaa40b8a 100644
--- a/app/Repositories/Budget/BudgetRepository.php
+++ b/app/Repositories/Budget/BudgetRepository.php
@@ -44,6 +44,7 @@ use FireflyIII\Support\Facades\Steam;
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
use FireflyIII\Support\Repositories\UserGroup\UserGroupInterface;
use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait;
+use FireflyIII\Support\Singleton\PreferencesSingleton;
use Illuminate\Database\QueryException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
@@ -85,7 +86,7 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
public function budgetedInPeriod(Carbon $start, Carbon $end): array
{
- app('log')->debug(sprintf('Now in budgetedInPeriod("%s", "%s")', $start->format('Y-m-d'), $end->format('Y-m-d')));
+ Log::debug(sprintf('Now in budgetedInPeriod("%s", "%s")', $start->format('Y-m-d'), $end->format('Y-m-d')));
$return = [];
/** @var BudgetLimitRepository $limitRepository */
@@ -97,12 +98,12 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
/** @var Budget $budget */
foreach ($budgets as $budget) {
- app('log')->debug(sprintf('Budget #%d: "%s"', $budget->id, $budget->name));
+ Log::debug(sprintf('Budget #%d: "%s"', $budget->id, $budget->name));
$limits = $limitRepository->getBudgetLimits($budget, $start, $end);
/** @var BudgetLimit $limit */
foreach ($limits as $limit) {
- app('log')->debug(sprintf('Budget limit #%d', $limit->id));
+ Log::debug(sprintf('Budget limit #%d', $limit->id));
$currency = $limit->transactionCurrency;
$rate = $converter->getCurrencyRate($currency, $primaryCurrency, $end);
$currencyCode = $currency->code;
@@ -124,7 +125,7 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
if ($limit->start_date->isSameDay($start) && $limit->end_date->isSameDay($end)) {
$return[$currencyCode]['sum'] = bcadd($return[$currencyCode]['sum'], (string) $limit->amount);
$return[$currencyCode]['pc_sum'] = bcmul($rate, $return[$currencyCode]['sum']);
- app('log')->debug(sprintf('Add full amount [1]: %s', $limit->amount));
+ Log::debug(sprintf('Add full amount [1]: %s', $limit->amount));
continue;
}
@@ -132,7 +133,7 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
if ($start->lte($limit->start_date) && $end->gte($limit->end_date)) {
$return[$currencyCode]['sum'] = bcadd($return[$currencyCode]['sum'], (string) $limit->amount);
$return[$currencyCode]['pc_sum'] = bcmul($rate, $return[$currencyCode]['sum']);
- app('log')->debug(sprintf('Add full amount [2]: %s', $limit->amount));
+ Log::debug(sprintf('Add full amount [2]: %s', $limit->amount));
continue;
}
@@ -141,7 +142,7 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
$amount = bcmul(bcdiv((string) $limit->amount, (string) $total), (string) $days);
$return[$currencyCode]['sum'] = bcadd($return[$currencyCode]['sum'], $amount);
$return[$currencyCode]['pc_sum'] = bcmul($rate, $return[$currencyCode]['sum']);
- app('log')->debug(
+ Log::debug(
sprintf(
'Amount per day: %s (%s over %d days). Total amount for %d days: %s',
bcdiv((string) $limit->amount, (string) $total),
@@ -202,19 +203,19 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
public function budgetedInPeriodForBudget(Budget $budget, Carbon $start, Carbon $end): array
{
- app('log')->debug(sprintf('Now in budgetedInPeriod(#%d, "%s", "%s")', $budget->id, $start->format('Y-m-d'), $end->format('Y-m-d')));
+ Log::debug(sprintf('Now in budgetedInPeriod(#%d, "%s", "%s")', $budget->id, $start->format('Y-m-d'), $end->format('Y-m-d')));
$return = [];
/** @var BudgetLimitRepository $limitRepository */
$limitRepository = app(BudgetLimitRepository::class);
$limitRepository->setUser($this->user);
- app('log')->debug(sprintf('Budget #%d: "%s"', $budget->id, $budget->name));
+ Log::debug(sprintf('Budget #%d: "%s"', $budget->id, $budget->name));
$limits = $limitRepository->getBudgetLimits($budget, $start, $end);
/** @var BudgetLimit $limit */
foreach ($limits as $limit) {
- app('log')->debug(sprintf('Budget limit #%d', $limit->id));
+ Log::debug(sprintf('Budget limit #%d', $limit->id));
$currency = $limit->transactionCurrency;
$return[$currency->id] ??= [
'id' => (string) $currency->id,
@@ -227,14 +228,14 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
// same period
if ($limit->start_date->isSameDay($start) && $limit->end_date->isSameDay($end)) {
$return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], (string) $limit->amount);
- app('log')->debug(sprintf('Add full amount [1]: %s', $limit->amount));
+ Log::debug(sprintf('Add full amount [1]: %s', $limit->amount));
continue;
}
// limit is inside of date range
if ($start->lte($limit->start_date) && $end->gte($limit->end_date)) {
$return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], (string) $limit->amount);
- app('log')->debug(sprintf('Add full amount [2]: %s', $limit->amount));
+ Log::debug(sprintf('Add full amount [2]: %s', $limit->amount));
continue;
}
@@ -242,7 +243,7 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
$days = $this->daysInOverlap($limit, $start, $end);
$amount = bcmul(bcdiv((string) $limit->amount, (string) $total), (string) $days);
$return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], $amount);
- app('log')->debug(
+ Log::debug(
sprintf(
'Amount per day: %s (%s over %d days). Total amount for %d days: %s',
bcdiv((string) $limit->amount, (string) $total),
@@ -282,7 +283,11 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
*/
public function update(Budget $budget, array $data): Budget
{
- app('log')->debug('Now in update()');
+ Log::debug('Now in update()');
+
+ // this is a lame trick to communicate with the observer.
+ $singleton = PreferencesSingleton::getInstance();
+ $singleton->setPreference('fire_webhooks_budget_update', $data['fire_webhooks'] ?? true);
$oldName = $budget->name;
if (array_key_exists('name', $data)) {
@@ -330,13 +335,13 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
->where('rule_actions.action_value', $oldName)
->get(['rule_actions.*'])
;
- app('log')->debug(sprintf('Found %d actions to update.', $actions->count()));
+ Log::debug(sprintf('Found %d actions to update.', $actions->count()));
/** @var RuleAction $action */
foreach ($actions as $action) {
$action->action_value = $newName;
$action->save();
- app('log')->debug(sprintf('Updated action %d: %s', $action->id, $action->action_value));
+ Log::debug(sprintf('Updated action %d: %s', $action->id, $action->action_value));
}
}
@@ -349,13 +354,13 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
->where('rule_triggers.trigger_value', $oldName)
->get(['rule_triggers.*'])
;
- app('log')->debug(sprintf('Found %d triggers to update.', $triggers->count()));
+ Log::debug(sprintf('Found %d triggers to update.', $triggers->count()));
/** @var RuleTrigger $trigger */
foreach ($triggers as $trigger) {
$trigger->trigger_value = $newName;
$trigger->save();
- app('log')->debug(sprintf('Updated trigger %d: %s', $trigger->id, $trigger->trigger_value));
+ Log::debug(sprintf('Updated trigger %d: %s', $trigger->id, $trigger->trigger_value));
}
}
@@ -486,17 +491,17 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
public function findBudget(?int $budgetId, ?string $budgetName): ?Budget
{
- app('log')->debug('Now in findBudget()');
- app('log')->debug(sprintf('Searching for budget with ID #%d...', $budgetId));
+ Log::debug('Now in findBudget()');
+ Log::debug(sprintf('Searching for budget with ID #%d...', $budgetId));
$result = $this->find((int) $budgetId);
if (!$result instanceof Budget && null !== $budgetName && '' !== $budgetName) {
- app('log')->debug(sprintf('Searching for budget with name %s...', $budgetName));
+ Log::debug(sprintf('Searching for budget with name %s...', $budgetName));
$result = $this->findByName($budgetName);
}
if ($result instanceof Budget) {
- app('log')->debug(sprintf('Found budget #%d: %s', $result->id, $result->name));
+ Log::debug(sprintf('Found budget #%d: %s', $result->id, $result->name));
}
- app('log')->debug(sprintf('Found result is null? %s', var_export(!$result instanceof Budget, true)));
+ Log::debug(sprintf('Found result is null? %s', var_export(!$result instanceof Budget, true)));
return $result;
}
@@ -593,7 +598,7 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
public function spentInPeriod(Carbon $start, Carbon $end): array
{
- app('log')->debug(sprintf('Now in %s', __METHOD__));
+ Log::debug(sprintf('Now in %s', __METHOD__));
$start->startOfDay();
$end->endOfDay();
@@ -655,7 +660,7 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
public function spentInPeriodForBudget(Budget $budget, Carbon $start, Carbon $end): array
{
- app('log')->debug(sprintf('Now in %s', __METHOD__));
+ Log::debug(sprintf('Now in %s', __METHOD__));
$start->startOfDay();
$end->endOfDay();
@@ -724,6 +729,10 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
{
$order = $this->getMaxOrder();
+ // this is a lame trick to communicate with the observer.
+ $singleton = PreferencesSingleton::getInstance();
+ $singleton->setPreference('fire_webhooks_budget_create', $data['fire_webhooks'] ?? true);
+
try {
$newBudget = Budget::create(
[
@@ -735,8 +744,8 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
]
);
} catch (QueryException $e) {
- app('log')->error($e->getMessage());
- app('log')->error($e->getTraceAsString());
+ Log::error($e->getMessage());
+ Log::error($e->getTraceAsString());
throw new FireflyException('400002: Could not store budget.', 0, $e);
}
diff --git a/app/Repositories/Budget/OperationsRepository.php b/app/Repositories/Budget/OperationsRepository.php
index 211c4fa1f5..badde3a9fb 100644
--- a/app/Repositories/Budget/OperationsRepository.php
+++ b/app/Repositories/Budget/OperationsRepository.php
@@ -314,7 +314,7 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
#[Override]
public function collectExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $budgets = null, ?TransactionCurrency $currency = null): array
{
- Log::debug(sprintf('Start of %s(date, date, array, array, "%s").', __METHOD__, $currency?->code));
+ Log::debug(sprintf('Start of %s(%s, %s, array, array, "%s").', __METHOD__, $start->toW3cString(), $end->toW3cString(), $currency?->code));
// this collector excludes all transfers TO liabilities (which are also withdrawals)
// because those expenses only become expenses once they move from the liability to the friend.
// 2024-12-24 disable the exclusion for now.
diff --git a/app/Repositories/Category/CategoryRepository.php b/app/Repositories/Category/CategoryRepository.php
index 487a467c20..894690e9e6 100644
--- a/app/Repositories/Category/CategoryRepository.php
+++ b/app/Repositories/Category/CategoryRepository.php
@@ -358,4 +358,43 @@ class CategoryRepository implements CategoryRepositoryInterface, UserGroupInterf
return $service->update($category, $data);
}
+
+ public function periodCollection(Category $category, Carbon $start, Carbon $end): array
+ {
+ Log::debug(sprintf('periodCollection(#%d, %s, %s)', $category->id, $start->format('Y-m-d'), $end->format('Y-m-d')));
+
+ return $category->transactionJournals()
+ ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
+ ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
+ ->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id')
+ ->leftJoin('transaction_currencies as foreign_currencies', 'foreign_currencies.id', '=', 'transactions.foreign_currency_id')
+ ->where('transaction_journals.date', '>=', $start)
+ ->where('transaction_journals.date', '<=', $end)
+ ->where('transactions.amount', '>', 0)
+ ->get([
+ // currencies
+ 'transaction_currencies.id as currency_id',
+ 'transaction_currencies.code as currency_code',
+ 'transaction_currencies.name as currency_name',
+ 'transaction_currencies.symbol as currency_symbol',
+ 'transaction_currencies.decimal_places as currency_decimal_places',
+
+ // foreign
+ 'foreign_currencies.id as foreign_currency_id',
+ 'foreign_currencies.code as foreign_currency_code',
+ 'foreign_currencies.name as foreign_currency_name',
+ 'foreign_currencies.symbol as foreign_currency_symbol',
+ 'foreign_currencies.decimal_places as foreign_currency_decimal_places',
+
+ // fields
+ 'transaction_journals.date',
+ 'transaction_types.type',
+ 'transaction_journals.transaction_currency_id',
+ 'transactions.amount',
+ 'transactions.native_amount as pc_amount',
+ 'transactions.foreign_amount',
+ ])
+ ->toArray()
+ ;
+ }
}
diff --git a/app/Repositories/Category/CategoryRepositoryInterface.php b/app/Repositories/Category/CategoryRepositoryInterface.php
index 263c11c716..cef58d2d17 100644
--- a/app/Repositories/Category/CategoryRepositoryInterface.php
+++ b/app/Repositories/Category/CategoryRepositoryInterface.php
@@ -48,6 +48,8 @@ interface CategoryRepositoryInterface
public function categoryStartsWith(string $query, int $limit): Collection;
+ public function periodCollection(Category $category, Carbon $start, Carbon $end): array;
+
public function destroy(Category $category): bool;
/**
diff --git a/app/Repositories/Category/OperationsRepository.php b/app/Repositories/Category/OperationsRepository.php
index e020782f40..a1fe709f4c 100644
--- a/app/Repositories/Category/OperationsRepository.php
+++ b/app/Repositories/Category/OperationsRepository.php
@@ -510,9 +510,7 @@ class OperationsRepository implements OperationsRepositoryInterface, UserGroupIn
$summarizer->setConvertToPrimary($convertToPrimary);
// filter $journals by range AND currency if it is present.
- $expenses = array_filter($expenses, static function (array $expense) use ($category): bool {
- return $expense['category_id'] === $category->id;
- });
+ $expenses = array_filter($expenses, static fn (array $expense): bool => $expense['category_id'] === $category->id);
return $summarizer->groupByCurrencyId($expenses, $method, false);
}
diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php
new file mode 100644
index 0000000000..3606ddf0d8
--- /dev/null
+++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepository.php
@@ -0,0 +1,132 @@
+.
+ */
+
+namespace FireflyIII\Repositories\PeriodStatistic;
+
+use Carbon\Carbon;
+use FireflyIII\Models\PeriodStatistic;
+use FireflyIII\Support\Repositories\UserGroup\UserGroupInterface;
+use FireflyIII\Support\Repositories\UserGroup\UserGroupTrait;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Log;
+
+class PeriodStatisticRepository implements PeriodStatisticRepositoryInterface, UserGroupInterface
+{
+ use UserGroupTrait;
+
+ public function findPeriodStatistics(Model $model, Carbon $start, Carbon $end, array $types): Collection
+ {
+ return $model->primaryPeriodStatistics()
+ ->where('start', $start)
+ ->where('end', $end)
+ ->whereIn('type', $types)
+ ->get();
+ }
+
+ public function findPeriodStatistic(Model $model, Carbon $start, Carbon $end, string $type): Collection
+ {
+ return $model->primaryPeriodStatistics()
+ ->where('start', $start)
+ ->where('end', $end)
+ ->where('type', $type)
+ ->get();
+ }
+
+ public function saveStatistic(Model $model, int $currencyId, Carbon $start, Carbon $end, string $type, int $count, string $amount): PeriodStatistic
+ {
+ $stat = new PeriodStatistic();
+ $stat->primaryStatable()->associate($model);
+ $stat->transaction_currency_id = $currencyId;
+ $stat->user_group_id = $this->getUserGroup()->id;
+ $stat->start = $start;
+ $stat->start_tz = $start->format('e');
+ $stat->end = $end;
+ $stat->end_tz = $end->format('e');
+ $stat->amount = $amount;
+ $stat->count = $count;
+ $stat->type = $type;
+ $stat->save();
+
+ Log::debug(sprintf(
+ 'Saved #%d [currency #%d, Model %s #%d, %s to %s, %d, %s] as new statistic.',
+ $stat->id,
+ $model::class,
+ $model->id,
+ $stat->transaction_currency_id,
+ $stat->start->toW3cString(),
+ $stat->end->toW3cString(),
+ $count,
+ $amount
+ ));
+
+ return $stat;
+ }
+
+ public function allInRangeForModel(Model $model, Carbon $start, Carbon $end): Collection
+ {
+ return $model->primaryPeriodStatistics()->where('start', '>=', $start)->where('end', '<=', $end)->get();
+ }
+
+ public function deleteStatisticsForModel(Model $model, Carbon $date): void
+ {
+ $model->primaryPeriodStatistics()->where('start', '<=', $date)->where('end', '>=', $date)->delete();
+ }
+
+ #[\Override]
+ public function allInRangeForPrefix(string $prefix, Carbon $start, Carbon $end): Collection
+ {
+ return $this->userGroup->periodStatistics()
+ ->where('type', 'LIKE', sprintf('%s%%', $prefix))
+ ->where('start', '>=', $start)->where('end', '<=', $end)->get();
+ }
+
+ #[\Override]
+ public function savePrefixedStatistic(string $prefix, int $currencyId, Carbon $start, Carbon $end, string $type, int $count, string $amount): PeriodStatistic
+ {
+ $stat = new PeriodStatistic();
+ $stat->transaction_currency_id = $currencyId;
+ $stat->user_group_id = $this->getUserGroup()->id;
+ $stat->start = $start;
+ $stat->start_tz = $start->format('e');
+ $stat->end = $end;
+ $stat->end_tz = $end->format('e');
+ $stat->amount = $amount;
+ $stat->count = $count;
+ $stat->type = sprintf('%s_%s',$prefix, $type);
+ $stat->save();
+
+ Log::debug(sprintf(
+ 'Saved #%d [currency #%d, type "%s", %s to %s, %d, %s] as new statistic.',
+ $stat->id,
+ $stat->transaction_currency_id,
+ $stat->type,
+ $stat->start->toW3cString(),
+ $stat->end->toW3cString(),
+ $count,
+ $amount
+ ));
+
+ return $stat;
+ }
+}
diff --git a/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php b/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php
new file mode 100644
index 0000000000..fb464e9794
--- /dev/null
+++ b/app/Repositories/PeriodStatistic/PeriodStatisticRepositoryInterface.php
@@ -0,0 +1,44 @@
+.
+ */
+
+namespace FireflyIII\Repositories\PeriodStatistic;
+
+use Carbon\Carbon;
+use FireflyIII\Models\PeriodStatistic;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Collection;
+
+interface PeriodStatisticRepositoryInterface
+{
+ public function findPeriodStatistics(Model $model, Carbon $start, Carbon $end, array $types): Collection;
+
+ public function findPeriodStatistic(Model $model, Carbon $start, Carbon $end, string $type): Collection;
+
+ public function saveStatistic(Model $model, int $currencyId, Carbon $start, Carbon $end, string $type, int $count, string $amount): PeriodStatistic;
+ public function savePrefixedStatistic(string $prefix, int $currencyId, Carbon $start, Carbon $end, string $type, int $count, string $amount): PeriodStatistic;
+
+ public function allInRangeForModel(Model $model, Carbon $start, Carbon $end): Collection;
+ public function allInRangeForPrefix(string $prefix, Carbon $start, Carbon $end): Collection;
+
+ public function deleteStatisticsForModel(Model $model, Carbon $date): void;
+}
diff --git a/app/Repositories/PiggyBank/ModifiesPiggyBanks.php b/app/Repositories/PiggyBank/ModifiesPiggyBanks.php
index 0acb4b7ed8..e63da28927 100644
--- a/app/Repositories/PiggyBank/ModifiesPiggyBanks.php
+++ b/app/Repositories/PiggyBank/ModifiesPiggyBanks.php
@@ -67,7 +67,7 @@ trait ModifiesPiggyBanks
{
$currentAmount = $this->getCurrentAmount($piggyBank, $account);
$pivot = $piggyBank->accounts()->where('accounts.id', $account->id)->first()->pivot;
- $pivot->current_amount = bcsub($currentAmount, $amount);
+ $pivot->current_amount = bcsub((string) $currentAmount, $amount);
$pivot->native_current_amount = null;
// also update native_current_amount.
@@ -90,7 +90,7 @@ trait ModifiesPiggyBanks
{
$currentAmount = $this->getCurrentAmount($piggyBank, $account);
$pivot = $piggyBank->accounts()->where('accounts.id', $account->id)->first()->pivot;
- $pivot->current_amount = bcadd($currentAmount, $amount);
+ $pivot->current_amount = bcadd((string) $currentAmount, $amount);
$pivot->native_current_amount = null;
// also update native_current_amount.
@@ -122,13 +122,13 @@ trait ModifiesPiggyBanks
if (0 !== bccomp($piggyBank->target_amount, '0')) {
- $leftToSave = bcsub($piggyBank->target_amount, $savedSoFar);
- $maxAmount = 1 === bccomp($leftOnAccount, $leftToSave) ? $leftToSave : $leftOnAccount;
+ $leftToSave = bcsub($piggyBank->target_amount, (string) $savedSoFar);
+ $maxAmount = 1 === bccomp((string) $leftOnAccount, $leftToSave) ? $leftToSave : $leftOnAccount;
Log::debug(sprintf('Left to save: %s', $leftToSave));
Log::debug(sprintf('Maximum amount: %s', $maxAmount));
}
- $compare = bccomp($amount, $maxAmount);
+ $compare = bccomp($amount, (string) $maxAmount);
$result = $compare <= 0;
Log::debug(sprintf('Compare <= 0? %d, so canAddAmount is %s', $compare, var_export($result, true)));
@@ -140,7 +140,7 @@ trait ModifiesPiggyBanks
{
$savedSoFar = $this->getCurrentAmount($piggyBank, $account);
- return bccomp($amount, $savedSoFar) <= 0;
+ return bccomp($amount, (string) $savedSoFar) <= 0;
}
/**
@@ -234,9 +234,9 @@ trait ModifiesPiggyBanks
// if the piggy bank is now smaller than the sum of the money saved,
// remove money from all accounts until the piggy bank is the right amount.
$currentAmount = $this->getCurrentAmount($piggyBank);
- if (1 === bccomp($currentAmount, (string)$piggyBank->target_amount) && 0 !== bccomp((string)$piggyBank->target_amount, '0')) {
+ if (1 === bccomp((string) $currentAmount, (string)$piggyBank->target_amount) && 0 !== bccomp((string)$piggyBank->target_amount, '0')) {
Log::debug(sprintf('Current amount is %s, target amount is %s', $currentAmount, $piggyBank->target_amount));
- $difference = bcsub((string)$piggyBank->target_amount, $currentAmount);
+ $difference = bcsub((string)$piggyBank->target_amount, (string) $currentAmount);
// an amount will be removed, create "negative" event:
// Log::debug(sprintf('ChangedAmount: is triggered with difference "%s"', $difference));
diff --git a/app/Repositories/Tag/TagRepository.php b/app/Repositories/Tag/TagRepository.php
index 58c998e760..eb50e2ef8f 100644
--- a/app/Repositories/Tag/TagRepository.php
+++ b/app/Repositories/Tag/TagRepository.php
@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Repositories\Tag;
+use Override;
use Carbon\Carbon;
use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Factory\TagFactory;
@@ -379,4 +380,44 @@ class TagRepository implements TagRepositoryInterface, UserGroupInterface
/** @var null|Location */
return $tag->locations()->first();
}
+
+ #[Override]
+ public function periodCollection(Tag $tag, Carbon $start, Carbon $end): array
+ {
+ Log::debug(sprintf('periodCollection(#%d, %s, %s)', $tag->id, $start->format('Y-m-d'), $end->format('Y-m-d')));
+
+ return $tag->transactionJournals()
+ ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
+ ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
+ ->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id')
+ ->leftJoin('transaction_currencies as foreign_currencies', 'foreign_currencies.id', '=', 'transactions.foreign_currency_id')
+ ->where('transaction_journals.date', '>=', $start)
+ ->where('transaction_journals.date', '<=', $end)
+ ->where('transactions.amount', '>', 0)
+ ->get([
+ // currencies
+ 'transaction_currencies.id as currency_id',
+ 'transaction_currencies.code as currency_code',
+ 'transaction_currencies.name as currency_name',
+ 'transaction_currencies.symbol as currency_symbol',
+ 'transaction_currencies.decimal_places as currency_decimal_places',
+
+ // foreign
+ 'foreign_currencies.id as foreign_currency_id',
+ 'foreign_currencies.code as foreign_currency_code',
+ 'foreign_currencies.name as foreign_currency_name',
+ 'foreign_currencies.symbol as foreign_currency_symbol',
+ 'foreign_currencies.decimal_places as foreign_currency_decimal_places',
+
+ // fields
+ 'transaction_journals.date',
+ 'transaction_types.type',
+ 'transaction_journals.transaction_currency_id',
+ 'transactions.amount',
+ 'transactions.native_amount as pc_amount',
+ 'transactions.foreign_amount',
+ ])
+ ->toArray()
+ ;
+ }
}
diff --git a/app/Repositories/Tag/TagRepositoryInterface.php b/app/Repositories/Tag/TagRepositoryInterface.php
index 2052dff88c..5a53efe7fe 100644
--- a/app/Repositories/Tag/TagRepositoryInterface.php
+++ b/app/Repositories/Tag/TagRepositoryInterface.php
@@ -51,6 +51,8 @@ interface TagRepositoryInterface
*/
public function destroy(Tag $tag): bool;
+ public function periodCollection(Tag $tag, Carbon $start, Carbon $end): array;
+
/**
* Destroy all tags.
*/
diff --git a/app/Repositories/TransactionGroup/TransactionGroupRepository.php b/app/Repositories/TransactionGroup/TransactionGroupRepository.php
index fec5d3f42f..c8c03b7a38 100644
--- a/app/Repositories/TransactionGroup/TransactionGroupRepository.php
+++ b/app/Repositories/TransactionGroup/TransactionGroupRepository.php
@@ -370,8 +370,11 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface,
public function getTagObjects(int $journalId): Collection
{
- /** @var TransactionJournal $journal */
+ /** @var null|TransactionJournal $journal */
$journal = $this->user->transactionJournals()->find($journalId);
+ if (null === $journal) {
+ return new Collection();
+ }
return $journal->tags()->whereNull('deleted_at')->get();
}
diff --git a/app/Services/Internal/Support/CreditRecalculateService.php b/app/Services/Internal/Support/CreditRecalculateService.php
index eb6b33b8d4..66b5fc359c 100644
--- a/app/Services/Internal/Support/CreditRecalculateService.php
+++ b/app/Services/Internal/Support/CreditRecalculateService.php
@@ -276,66 +276,66 @@ class CreditRecalculateService
if ($isSameAccount && $isCredit && $this->isWithdrawalIn($usedAmount, $type)) { // case 1
$usedAmount = app('steam')->positive($usedAmount);
- return bcadd($leftOfDebt, $usedAmount);
+ return bcadd($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 1 (withdrawal into credit liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isCredit && $this->isWithdrawalOut($usedAmount, $type)) { // case 2
$usedAmount = app('steam')->positive($usedAmount);
- return bcsub($leftOfDebt, $usedAmount);
+ return bcsub($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 2 (withdrawal away from liability): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isCredit && $this->isDepositOut($usedAmount, $type)) { // case 3
$usedAmount = app('steam')->positive($usedAmount);
- return bcsub($leftOfDebt, $usedAmount);
+ return bcsub($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 3 (deposit away from liability): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isCredit && $this->isDepositIn($usedAmount, $type)) { // case 4
$usedAmount = app('steam')->positive($usedAmount);
- return bcadd($leftOfDebt, $usedAmount);
+ return bcadd($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 4 (deposit into credit liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isCredit && $this->isTransferIn($usedAmount, $type)) { // case 5
$usedAmount = app('steam')->positive($usedAmount);
- return bcadd($leftOfDebt, $usedAmount);
+ return bcadd($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 5 (transfer into credit liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isDebit && $this->isWithdrawalIn($usedAmount, $type)) { // case 6
$usedAmount = app('steam')->positive($usedAmount);
- return bcsub($leftOfDebt, $usedAmount);
+ return bcsub($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 6 (withdrawal into debit liability): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isDebit && $this->isDepositOut($usedAmount, $type)) { // case 7
$usedAmount = app('steam')->positive($usedAmount);
- return bcadd($leftOfDebt, $usedAmount);
+ return bcadd($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 7 (deposit away from liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isDebit && $this->isWithdrawalOut($usedAmount, $type)) { // case 8
$usedAmount = app('steam')->positive($usedAmount);
- return bcadd($leftOfDebt, $usedAmount);
+ return bcadd($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case 8 (withdrawal away from liability): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isDebit && $this->isTransferIn($usedAmount, $type)) { // case 9
$usedAmount = app('steam')->positive($usedAmount);
- return bcsub($leftOfDebt, $usedAmount);
+ return bcsub($leftOfDebt, (string) $usedAmount);
// 2024-10-05, #9225 this used to say you would owe more, but a transfer INTO a debit from wherever means you owe LESS.
// Log::debug(sprintf('Case 9 (transfer into debit liability, means you owe LESS): %s - %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
if ($isSameAccount && $isDebit && $this->isTransferOut($usedAmount, $type)) { // case 10
$usedAmount = app('steam')->positive($usedAmount);
- return bcadd($leftOfDebt, $usedAmount);
+ return bcadd($leftOfDebt, (string) $usedAmount);
// 2024-10-05, #9225 this used to say you would owe less, but a transfer OUT OF a debit from wherever means you owe MORE.
// Log::debug(sprintf('Case 10 (transfer out of debit liability, means you owe MORE): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
@@ -344,7 +344,7 @@ class CreditRecalculateService
if (in_array($type, [TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::DEPOSIT->value, TransactionTypeEnum::TRANSFER->value], true)) {
$usedAmount = app('steam')->negative($usedAmount);
- return bcadd($leftOfDebt, $usedAmount);
+ return bcadd($leftOfDebt, (string) $usedAmount);
// Log::debug(sprintf('Case X (all other cases): %s + %s = %s', app('steam')->bcround($leftOfDebt, 2), app('steam')->bcround($usedAmount, 2), app('steam')->bcround($result, 2)));
}
diff --git a/app/Services/Internal/Support/JournalServiceTrait.php b/app/Services/Internal/Support/JournalServiceTrait.php
index d0de5db9df..8cd15203ca 100644
--- a/app/Services/Internal/Support/JournalServiceTrait.php
+++ b/app/Services/Internal/Support/JournalServiceTrait.php
@@ -54,7 +54,7 @@ trait JournalServiceTrait
/**
* @throws FireflyException
*/
- protected function getAccount(string $transactionType, string $direction, array $data): ?Account
+ protected function getAccount(string $transactionType, string $direction, array $data, ?Account $opposite = null): ?Account
{
// some debug logging:
Log::debug(sprintf('Now in getAccount(%s)', $direction), $data);
@@ -69,12 +69,12 @@ trait JournalServiceTrait
$message = 'Transaction = %s, %s account should be in: %s. Direction is %s.';
Log::debug(sprintf($message, $transactionType, $direction, implode(', ', $expectedTypes[$transactionType] ?? ['UNKNOWN']), $direction));
- $result = $this->findAccountById($data, $expectedTypes[$transactionType]);
- $result = $this->findAccountByIban($result, $data, $expectedTypes[$transactionType]);
+ $result = $this->findAccountById($data, $expectedTypes[$transactionType], $opposite);
+ $result = $this->findAccountByIban($result, $data, $expectedTypes[$transactionType], $opposite);
$ibanResult = $result;
- $result = $this->findAccountByNumber($result, $data, $expectedTypes[$transactionType]);
+ $result = $this->findAccountByNumber($result, $data, $expectedTypes[$transactionType], $opposite);
$numberResult = $result;
- $result = $this->findAccountByName($result, $data, $expectedTypes[$transactionType]);
+ $result = $this->findAccountByName($result, $data, $expectedTypes[$transactionType], $opposite);
$nameResult = $result;
// if $result (find by name) is NULL, but IBAN is set, any result of the search by NAME can't overrule
@@ -82,7 +82,7 @@ trait JournalServiceTrait
if (null !== $nameResult && null === $numberResult && null === $ibanResult && '' !== (string) $data['iban'] && '' !== (string) $nameResult->iban) {
$data['name'] = sprintf('%s (%s)', $data['name'], $data['iban']);
Log::debug(sprintf('Search again using the new name, "%s".', $data['name']));
- $result = $this->findAccountByName(null, $data, $expectedTypes[$transactionType]);
+ $result = $this->findAccountByName(null, $data, $expectedTypes[$transactionType], $opposite);
}
// the account that Firefly III creates must be "creatable", aka select the one we can create from the list just in case
@@ -115,15 +115,19 @@ trait JournalServiceTrait
return $result;
}
- private function findAccountById(array $data, array $types): ?Account
+ private function findAccountById(array $data, array $types, ?Account $opposite = null): ?Account
{
// first attempt, find by ID.
if (null !== $data['id']) {
$search = $this->accountRepository->find((int) $data['id']);
if (null !== $search && in_array($search->accountType->type, $types, true)) {
- Log::debug(
- sprintf('Found "account_id" object: #%d, "%s" of type %s (1)', $search->id, $search->name, $search->accountType->type)
- );
+ Log::debug(sprintf('Found "account_id" object: #%d, "%s" of type %s (1)', $search->id, $search->name, $search->accountType->type));
+
+ if ($opposite?->id === $search->id) {
+ Log::debug(sprintf('Account #%d is the same as opposite account #%d, returning NULL.', $search->id, $opposite->id));
+
+ return null;
+ }
return $search;
}
@@ -140,7 +144,7 @@ trait JournalServiceTrait
return null;
}
- private function findAccountByIban(?Account $account, array $data, array $types): ?Account
+ private function findAccountByIban(?Account $account, array $data, array $types, ?Account $opposite = null): ?Account
{
if ($account instanceof Account) {
Log::debug(sprintf('Already have account #%d ("%s"), return that.', $account->id, $account->name));
@@ -153,21 +157,27 @@ trait JournalServiceTrait
return null;
}
// find by preferred type.
- $source = $this->accountRepository->findByIbanNull($data['iban'], [$types[0]]);
+ $result = $this->accountRepository->findByIbanNull($data['iban'], [$types[0]]);
// or any expected type.
- $source ??= $this->accountRepository->findByIbanNull($data['iban'], $types);
+ $result ??= $this->accountRepository->findByIbanNull($data['iban'], $types);
- if (null !== $source) {
- Log::debug(sprintf('Found "account_iban" object: #%d, %s', $source->id, $source->name));
+ if (null !== $result) {
+ Log::debug(sprintf('Found "account_iban" object: #%d, %s', $result->id, $result->name));
- return $source;
+ if ($opposite?->id === $result->id) {
+ Log::debug(sprintf('Account #%d is the same as opposite account #%d, returning NULL.', $result->id, $opposite->id));
+
+ return null;
+ }
+
+ return $result;
}
Log::debug(sprintf('Found no account with IBAN "%s" of expected types', $data['iban']), $types);
return null;
}
- private function findAccountByNumber(?Account $account, array $data, array $types): ?Account
+ private function findAccountByNumber(?Account $account, array $data, array $types, ?Account $opposite = null): ?Account
{
if ($account instanceof Account) {
Log::debug(sprintf('Already have account #%d ("%s"), return that.', $account->id, $account->name));
@@ -180,15 +190,21 @@ trait JournalServiceTrait
return null;
}
// find by preferred type.
- $source = $this->accountRepository->findByAccountNumber((string) $data['number'], [$types[0]]);
+ $result = $this->accountRepository->findByAccountNumber((string) $data['number'], [$types[0]]);
// or any expected type.
- $source ??= $this->accountRepository->findByAccountNumber((string) $data['number'], $types);
+ $result ??= $this->accountRepository->findByAccountNumber((string) $data['number'], $types);
- if (null !== $source) {
- Log::debug(sprintf('Found account: #%d, %s', $source->id, $source->name));
+ if (null !== $result) {
+ Log::debug(sprintf('Found account: #%d, %s', $result->id, $result->name));
- return $source;
+ if ($opposite?->id === $result->id) {
+ Log::debug(sprintf('Account #%d is the same as opposite account #%d, returning NULL.', $result->id, $opposite->id));
+
+ return null;
+ }
+
+ return $result;
}
Log::debug(sprintf('Found no account with account number "%s" of expected types', $data['number']), $types);
@@ -196,7 +212,7 @@ trait JournalServiceTrait
return null;
}
- private function findAccountByName(?Account $account, array $data, array $types): ?Account
+ private function findAccountByName(?Account $account, array $data, array $types, ?Account $opposite = null): ?Account
{
if ($account instanceof Account) {
Log::debug(sprintf('Already have account #%d ("%s"), return that.', $account->id, $account->name));
@@ -210,15 +226,21 @@ trait JournalServiceTrait
}
// find by preferred type.
- $source = $this->accountRepository->findByName($data['name'], [$types[0]]);
+ $result = $this->accountRepository->findByName($data['name'], [$types[0]]);
// or any expected type.
- $source ??= $this->accountRepository->findByName($data['name'], $types);
+ $result ??= $this->accountRepository->findByName($data['name'], $types);
- if (null !== $source) {
- Log::debug(sprintf('Found "account_name" object: #%d, %s', $source->id, $source->name));
+ if (null !== $result) {
+ Log::debug(sprintf('Found "account_name" object: #%d, %s', $result->id, $result->name));
- return $source;
+ if ($opposite?->id === $result->id) {
+ Log::debug(sprintf('Account #%d is the same as opposite account #%d, returning NULL.', $result->id, $opposite->id));
+
+ return null;
+ }
+
+ return $result;
}
Log::debug(sprintf('Found no account with account name "%s" of expected types', $data['name']), $types);
diff --git a/app/Support/Amount.php b/app/Support/Amount.php
index 547cf8dd79..98e8aa284c 100644
--- a/app/Support/Amount.php
+++ b/app/Support/Amount.php
@@ -41,280 +41,6 @@ use NumberFormatter;
*/
class Amount
{
- /**
- * This method will properly format the given number, in color or "black and white",
- * as a currency, given two things: the currency required and the current locale.
- *
- * @throws FireflyException
- */
- public function formatAnything(TransactionCurrency $format, string $amount, ?bool $coloured = null): string
- {
- return $this->formatFlat($format->symbol, $format->decimal_places, $amount, $coloured);
- }
-
- /**
- * This method will properly format the given number, in color or "black and white",
- * as a currency, given two things: the currency required and the current locale.
- *
- * @throws FireflyException
- */
- public function formatFlat(string $symbol, int $decimalPlaces, string $amount, ?bool $coloured = null): string
- {
- $locale = Steam::getLocale();
- $rounded = Steam::bcround($amount, $decimalPlaces);
- $coloured ??= true;
-
- $fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY);
- $fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $symbol);
- $fmt->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimalPlaces);
- $fmt->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimalPlaces);
- $result = (string)$fmt->format((float)$rounded); // intentional float
-
- if (true === $coloured) {
- if (1 === bccomp($rounded, '0')) {
- return sprintf('%s', $result);
- }
- if (-1 === bccomp($rounded, '0')) {
- return sprintf('%s', $result);
- }
-
- return sprintf('%s', $result);
- }
-
- return $result;
- }
-
- public function formatByCurrencyId(int $currencyId, string $amount, ?bool $coloured = null): string
- {
- $format = $this->getTransactionCurrencyById($currencyId);
-
- return $this->formatFlat($format->symbol, $format->decimal_places, $amount, $coloured);
- }
-
- public function getAllCurrencies(): Collection
- {
- return TransactionCurrency::orderBy('code', 'ASC')->get();
- }
-
- /**
- * Experimental function to see if we can quickly and quietly get the amount from a journal.
- * This depends on the user's default currency and the wish to have it converted.
- */
- public function getAmountFromJournal(array $journal): string
- {
- $convertToPrimary = $this->convertToPrimary();
- $currency = $this->getPrimaryCurrency();
- $field = $convertToPrimary && $currency->id !== $journal['currency_id'] ? 'pc_amount' : 'amount';
- $amount = $journal[$field] ?? '0';
- // Log::debug(sprintf('Field is %s, amount is %s', $field, $amount));
- // fallback, the transaction has a foreign amount in $currency.
- if ($convertToPrimary && null !== $journal['foreign_amount'] && $currency->id === (int)$journal['foreign_currency_id']) {
- $amount = $journal['foreign_amount'];
- // Log::debug(sprintf('Overruled, amount is now %s', $amount));
- }
-
- return (string)$amount;
- }
-
- public function getTransactionCurrencyById(int $currencyId): TransactionCurrency
- {
- $instance = PreferencesSingleton::getInstance();
- $key = sprintf('transaction_currency_%d', $currencyId);
-
- /** @var null|TransactionCurrency $pref */
- $pref = $instance->getPreference($key);
- if (null !== $pref) {
- return $pref;
- }
- $currency = TransactionCurrency::find($currencyId);
- if (null === $currency) {
- $message = sprintf('Could not find a transaction currency with ID #%d', $currencyId);
- Log::error($message);
-
- throw new FireflyException($message);
- }
- $instance->setPreference($key, $currency);
-
- return $currency;
- }
-
- public function getTransactionCurrencyByCode(string $code): TransactionCurrency
- {
- $instance = PreferencesSingleton::getInstance();
- $key = sprintf('transaction_currency_%s', $code);
-
- /** @var null|TransactionCurrency $pref */
- $pref = $instance->getPreference($key);
- if (null !== $pref) {
- return $pref;
- }
- $currency = TransactionCurrency::whereCode($code)->first();
- if (null === $currency) {
- $message = sprintf('Could not find a transaction currency with code "%s"', $code);
- Log::error($message);
-
- throw new FireflyException($message);
- }
- $instance->setPreference($key, $currency);
-
- return $currency;
- }
-
- public function convertToPrimary(?User $user = null): bool
- {
- $instance = PreferencesSingleton::getInstance();
- if (!$user instanceof User) {
- $pref = $instance->getPreference('convert_to_primary_no_user');
- if (null === $pref) {
- $res = true === Preferences::get('convert_to_primary', false)->data && true === config('cer.enabled');
- $instance->setPreference('convert_to_primary_no_user', $res);
-
- return $res;
- }
-
- return $pref;
- }
- $key = sprintf('convert_to_primary_%d', $user->id);
- $pref = $instance->getPreference($key);
- if (null === $pref) {
- $res = true === Preferences::getForUser($user, 'convert_to_primary', false)->data && true === config('cer.enabled');
- $instance->setPreference($key, $res);
-
- return $res;
- }
-
- return $pref;
- }
-
- public function getPrimaryCurrency(): TransactionCurrency
- {
- if (auth()->check()) {
- /** @var User $user */
- $user = auth()->user();
- if (null !== $user->userGroup) {
- return $this->getPrimaryCurrencyByUserGroup($user->userGroup);
- }
- }
-
- return $this->getSystemCurrency();
- }
-
- public function getPrimaryCurrencyByUserGroup(UserGroup $userGroup): TransactionCurrency
- {
- $cache = new CacheProperties();
- $cache->addProperty('getPrimaryCurrencyByGroup');
- $cache->addProperty($userGroup->id);
- if ($cache->has()) {
- return $cache->get();
- }
-
- /** @var null|TransactionCurrency $primary */
- $primary = $userGroup->currencies()->where('group_default', true)->first();
- if (null === $primary) {
- $primary = $this->getSystemCurrency();
- // could be the user group has no default right now.
- $userGroup->currencies()->sync([$primary->id => ['group_default' => true]]);
- }
- $cache->store($primary);
-
- return $primary;
- }
-
- public function getSystemCurrency(): TransactionCurrency
- {
- return TransactionCurrency::whereNull('deleted_at')->where('code', 'EUR')->first();
- }
-
- /**
- * Experimental function to see if we can quickly and quietly get the amount from a journal.
- * This depends on the user's default currency and the wish to have it converted.
- */
- public function getAmountFromJournalObject(TransactionJournal $journal): string
- {
- $convertToPrimary = $this->convertToPrimary();
- $currency = $this->getPrimaryCurrency();
- $field = $convertToPrimary && $currency->id !== $journal->transaction_currency_id ? 'pc_amount' : 'amount';
-
- /** @var null|Transaction $sourceTransaction */
- $sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first();
- if (null === $sourceTransaction) {
- return '0';
- }
- $amount = $sourceTransaction->{$field} ?? '0';
- if ((int)$sourceTransaction->foreign_currency_id === $currency->id) {
- // use foreign amount instead!
- $amount = (string)$sourceTransaction->foreign_amount; // hard coded to be foreign amount.
- }
-
- return $amount;
- }
-
- public function getCurrencies(): Collection
- {
- /** @var User $user */
- $user = auth()->user();
-
- return $user->currencies()->orderBy('code', 'ASC')->get();
- }
-
- /**
- * This method returns the correct format rules required by accounting.js,
- * the library used to format amounts in charts.
- *
- * Used only in one place.
- *
- * @throws FireflyException
- */
- public function getJsConfig(): array
- {
- $config = $this->getLocaleInfo();
- $negative = self::getAmountJsConfig($config['n_sep_by_space'], $config['n_sign_posn'], $config['negative_sign'], $config['n_cs_precedes']);
- $positive = self::getAmountJsConfig($config['p_sep_by_space'], $config['p_sign_posn'], $config['positive_sign'], $config['p_cs_precedes']);
-
- return [
- 'mon_decimal_point' => $config['mon_decimal_point'],
- 'mon_thousands_sep' => $config['mon_thousands_sep'],
- 'format' => [
- 'pos' => $positive,
- 'neg' => $negative,
- 'zero' => $positive,
- ],
- ];
- }
-
- /**
- * @throws FireflyException
- */
- private function getLocaleInfo(): array
- {
- // get config from preference, not from translation:
- $locale = Steam::getLocale();
- $array = Steam::getLocaleArray($locale);
-
- setlocale(LC_MONETARY, $array);
- $info = localeconv();
-
- // correct variables
- $info['n_cs_precedes'] = $this->getLocaleField($info, 'n_cs_precedes');
- $info['p_cs_precedes'] = $this->getLocaleField($info, 'p_cs_precedes');
-
- $info['n_sep_by_space'] = $this->getLocaleField($info, 'n_sep_by_space');
- $info['p_sep_by_space'] = $this->getLocaleField($info, 'p_sep_by_space');
-
- $fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY);
-
- $info['mon_decimal_point'] = $fmt->getSymbol(NumberFormatter::MONETARY_SEPARATOR_SYMBOL);
- $info['mon_thousands_sep'] = $fmt->getSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL);
-
- return $info;
- }
-
- private function getLocaleField(array $info, string $field): bool
- {
- return (is_bool($info[$field]) && true === $info[$field])
- || (is_int($info[$field]) && 1 === $info[$field]);
- }
-
/**
* bool $sepBySpace is $localeconv['n_sep_by_space']
* int $signPosn = $localeconv['n_sign_posn']
@@ -384,4 +110,278 @@ class Amount
return $posA.$posD.'%v'.$space.$posB.'%s'.$posC.$posE;
}
+
+ public function convertToPrimary(?User $user = null): bool
+ {
+ $instance = PreferencesSingleton::getInstance();
+ if (!$user instanceof User) {
+ $pref = $instance->getPreference('convert_to_primary_no_user');
+ if (null === $pref) {
+ $res = true === Preferences::get('convert_to_primary', false)->data && true === config('cer.enabled');
+ $instance->setPreference('convert_to_primary_no_user', $res);
+
+ return $res;
+ }
+
+ return $pref;
+ }
+ $key = sprintf('convert_to_primary_%d', $user->id);
+ $pref = $instance->getPreference($key);
+ if (null === $pref) {
+ $res = true === Preferences::getForUser($user, 'convert_to_primary', false)->data && true === config('cer.enabled');
+ $instance->setPreference($key, $res);
+
+ return $res;
+ }
+
+ return $pref;
+ }
+
+ /**
+ * This method will properly format the given number, in color or "black and white",
+ * as a currency, given two things: the currency required and the current locale.
+ *
+ * @throws FireflyException
+ */
+ public function formatAnything(TransactionCurrency $format, string $amount, ?bool $coloured = null): string
+ {
+ return $this->formatFlat($format->symbol, $format->decimal_places, $amount, $coloured);
+ }
+
+ public function formatByCurrencyId(int $currencyId, string $amount, ?bool $coloured = null): string
+ {
+ $format = $this->getTransactionCurrencyById($currencyId);
+
+ return $this->formatFlat($format->symbol, $format->decimal_places, $amount, $coloured);
+ }
+
+ /**
+ * This method will properly format the given number, in color or "black and white",
+ * as a currency, given two things: the currency required and the current locale.
+ *
+ * @throws FireflyException
+ */
+ public function formatFlat(string $symbol, int $decimalPlaces, string $amount, ?bool $coloured = null): string
+ {
+ $locale = Steam::getLocale();
+ $rounded = Steam::bcround($amount, $decimalPlaces);
+ $coloured ??= true;
+
+ $fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY);
+ $fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $symbol);
+ $fmt->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimalPlaces);
+ $fmt->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimalPlaces);
+ $result = (string)$fmt->format((float)$rounded); // intentional float
+
+ if (true === $coloured) {
+ if (1 === bccomp($rounded, '0')) {
+ return sprintf('%s', $result);
+ }
+ if (-1 === bccomp($rounded, '0')) {
+ return sprintf('%s', $result);
+ }
+
+ return sprintf('%s', $result);
+ }
+
+ return $result;
+ }
+
+ public function getAllCurrencies(): Collection
+ {
+ return TransactionCurrency::orderBy('code', 'ASC')->get();
+ }
+
+ /**
+ * Experimental function to see if we can quickly and quietly get the amount from a journal.
+ * This depends on the user's default currency and the wish to have it converted.
+ */
+ public function getAmountFromJournal(array $journal): string
+ {
+ $convertToPrimary = $this->convertToPrimary();
+ $currency = $this->getPrimaryCurrency();
+ $field = $convertToPrimary && $currency->id !== $journal['currency_id'] ? 'pc_amount' : 'amount';
+ $amount = $journal[$field] ?? '0';
+ // Log::debug(sprintf('Field is %s, amount is %s', $field, $amount));
+ // fallback, the transaction has a foreign amount in $currency.
+ if ($convertToPrimary && null !== $journal['foreign_amount'] && $currency->id === (int)$journal['foreign_currency_id']) {
+ $amount = $journal['foreign_amount'];
+ // Log::debug(sprintf('Overruled, amount is now %s', $amount));
+ }
+
+ return (string)$amount;
+ }
+
+ /**
+ * Experimental function to see if we can quickly and quietly get the amount from a journal.
+ * This depends on the user's default currency and the wish to have it converted.
+ */
+ public function getAmountFromJournalObject(TransactionJournal $journal): string
+ {
+ $convertToPrimary = $this->convertToPrimary();
+ $currency = $this->getPrimaryCurrency();
+ $field = $convertToPrimary && $currency->id !== $journal->transaction_currency_id ? 'pc_amount' : 'amount';
+
+ /** @var null|Transaction $sourceTransaction */
+ $sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first();
+ if (null === $sourceTransaction) {
+ return '0';
+ }
+ $amount = $sourceTransaction->{$field} ?? '0';
+ if ((int)$sourceTransaction->foreign_currency_id === $currency->id) {
+ // use foreign amount instead!
+ $amount = (string)$sourceTransaction->foreign_amount; // hard coded to be foreign amount.
+ }
+
+ return $amount;
+ }
+
+ public function getCurrencies(): Collection
+ {
+ /** @var User $user */
+ $user = auth()->user();
+
+ return $user->currencies()->orderBy('code', 'ASC')->get();
+ }
+
+ /**
+ * This method returns the correct format rules required by accounting.js,
+ * the library used to format amounts in charts.
+ *
+ * Used only in one place.
+ *
+ * @throws FireflyException
+ */
+ public function getJsConfig(): array
+ {
+ $config = $this->getLocaleInfo();
+ $negative = self::getAmountJsConfig($config['n_sep_by_space'], $config['n_sign_posn'], $config['negative_sign'], $config['n_cs_precedes']);
+ $positive = self::getAmountJsConfig($config['p_sep_by_space'], $config['p_sign_posn'], $config['positive_sign'], $config['p_cs_precedes']);
+
+ return [
+ 'mon_decimal_point' => $config['mon_decimal_point'],
+ 'mon_thousands_sep' => $config['mon_thousands_sep'],
+ 'format' => [
+ 'pos' => $positive,
+ 'neg' => $negative,
+ 'zero' => $positive,
+ ],
+ ];
+ }
+
+ public function getPrimaryCurrency(): TransactionCurrency
+ {
+ if (auth()->check()) {
+ /** @var User $user */
+ $user = auth()->user();
+ if (null !== $user->userGroup) {
+ return $this->getPrimaryCurrencyByUserGroup($user->userGroup);
+ }
+ }
+
+ return $this->getSystemCurrency();
+ }
+
+ public function getPrimaryCurrencyByUserGroup(UserGroup $userGroup): TransactionCurrency
+ {
+ $cache = new CacheProperties();
+ $cache->addProperty('getPrimaryCurrencyByGroup');
+ $cache->addProperty($userGroup->id);
+ if ($cache->has()) {
+ return $cache->get();
+ }
+
+ /** @var null|TransactionCurrency $primary */
+ $primary = $userGroup->currencies()->where('group_default', true)->first();
+ if (null === $primary) {
+ $primary = $this->getSystemCurrency();
+ // could be the user group has no default right now.
+ $userGroup->currencies()->sync([$primary->id => ['group_default' => true]]);
+ }
+ $cache->store($primary);
+
+ return $primary;
+ }
+
+ public function getSystemCurrency(): TransactionCurrency
+ {
+ return TransactionCurrency::whereNull('deleted_at')->where('code', 'EUR')->first();
+ }
+
+ public function getTransactionCurrencyByCode(string $code): TransactionCurrency
+ {
+ $instance = PreferencesSingleton::getInstance();
+ $key = sprintf('transaction_currency_%s', $code);
+
+ /** @var null|TransactionCurrency $pref */
+ $pref = $instance->getPreference($key);
+ if (null !== $pref) {
+ return $pref;
+ }
+ $currency = TransactionCurrency::whereCode($code)->first();
+ if (null === $currency) {
+ $message = sprintf('Could not find a transaction currency with code "%s" in %s', $code, __METHOD__);
+ Log::error($message);
+
+ throw new FireflyException($message);
+ }
+ $instance->setPreference($key, $currency);
+
+ return $currency;
+ }
+
+ public function getTransactionCurrencyById(int $currencyId): TransactionCurrency
+ {
+ $instance = PreferencesSingleton::getInstance();
+ $key = sprintf('transaction_currency_%d', $currencyId);
+
+ /** @var null|TransactionCurrency $pref */
+ $pref = $instance->getPreference($key);
+ if (null !== $pref) {
+ return $pref;
+ }
+ $currency = TransactionCurrency::find($currencyId);
+ if (null === $currency) {
+ $message = sprintf('Could not find a transaction currency with ID #%d in %s', $currencyId, __METHOD__);
+ Log::error($message);
+
+ throw new FireflyException($message);
+ }
+ $instance->setPreference($key, $currency);
+
+ return $currency;
+ }
+
+ private function getLocaleField(array $info, string $field): bool
+ {
+ return (is_bool($info[$field]) && true === $info[$field])
+ || (is_int($info[$field]) && 1 === $info[$field]);
+ }
+
+ /**
+ * @throws FireflyException
+ */
+ private function getLocaleInfo(): array
+ {
+ // get config from preference, not from translation:
+ $locale = Steam::getLocale();
+ $array = Steam::getLocaleArray($locale);
+
+ setlocale(LC_MONETARY, $array);
+ $info = localeconv();
+
+ // correct variables
+ $info['n_cs_precedes'] = $this->getLocaleField($info, 'n_cs_precedes');
+ $info['p_cs_precedes'] = $this->getLocaleField($info, 'p_cs_precedes');
+
+ $info['n_sep_by_space'] = $this->getLocaleField($info, 'n_sep_by_space');
+ $info['p_sep_by_space'] = $this->getLocaleField($info, 'p_sep_by_space');
+
+ $fmt = new NumberFormatter($locale, NumberFormatter::CURRENCY);
+
+ $info['mon_decimal_point'] = $fmt->getSymbol(NumberFormatter::MONETARY_SEPARATOR_SYMBOL);
+ $info['mon_thousands_sep'] = $fmt->getSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL);
+
+ return $info;
+ }
}
diff --git a/app/Support/Authentication/RemoteUserGuard.php b/app/Support/Authentication/RemoteUserGuard.php
index c2e534184b..1c87806e00 100644
--- a/app/Support/Authentication/RemoteUserGuard.php
+++ b/app/Support/Authentication/RemoteUserGuard.php
@@ -86,7 +86,7 @@ class RemoteUserGuard implements Guard
$header = config('auth.guard_email');
if (null !== $header) {
- $emailAddress = (string) (request()->server($header) ?? apache_request_headers()[$header] ?? null);
+ $emailAddress = (string)(request()->server($header) ?? apache_request_headers()[$header] ?? null);
$preference = Preferences::getForUser($retrievedUser, 'remote_guard_alt_email');
if ('' !== $emailAddress && null === $preference && $emailAddress !== $userID) {
@@ -102,13 +102,6 @@ class RemoteUserGuard implements Guard
$this->user = $retrievedUser;
}
- public function guest(): bool
- {
- Log::debug(sprintf('Now at %s', __METHOD__));
-
- return !$this->check();
- }
-
public function check(): bool
{
Log::debug(sprintf('Now at %s', __METHOD__));
@@ -116,17 +109,11 @@ class RemoteUserGuard implements Guard
return $this->user() instanceof User;
}
- public function user(): ?User
+ public function guest(): bool
{
Log::debug(sprintf('Now at %s', __METHOD__));
- $user = $this->user;
- if (!$user instanceof User) {
- Log::debug('User is NULL');
- return null;
- }
-
- return $user;
+ return !$this->check();
}
public function hasUser(): bool
@@ -157,6 +144,19 @@ class RemoteUserGuard implements Guard
Log::error(sprintf('Did not set user at %s', __METHOD__));
}
+ public function user(): ?User
+ {
+ Log::debug(sprintf('Now at %s', __METHOD__));
+ $user = $this->user;
+ if (!$user instanceof User) {
+ Log::debug('User is NULL');
+
+ return null;
+ }
+
+ return $user;
+ }
+
/**
* @throws FireflyException
*
diff --git a/app/Support/Balance.php b/app/Support/Balance.php
index 6b97a04628..0a9a97d0ee 100644
--- a/app/Support/Balance.php
+++ b/app/Support/Balance.php
@@ -59,8 +59,8 @@ class Balance
$result = $query->get(['transactions.account_id', 'transactions.transaction_currency_id', 'transactions.balance_after']);
foreach ($result as $entry) {
- $accountId = (int) $entry->account_id;
- $currencyId = (int) $entry->transaction_currency_id;
+ $accountId = (int)$entry->account_id;
+ $currencyId = (int)$entry->transaction_currency_id;
$currencies[$currencyId] ??= Amount::getTransactionCurrencyById($currencyId);
$return[$accountId] ??= [];
if (array_key_exists($currencyId, $return[$accountId])) {
diff --git a/app/Support/Binder/TagList.php b/app/Support/Binder/TagList.php
index 3dd4835f54..d87c8c69b9 100644
--- a/app/Support/Binder/TagList.php
+++ b/app/Support/Binder/TagList.php
@@ -68,7 +68,7 @@ class TagList implements BinderInterface
return true;
}
- if (in_array((string) $tag->id, $list, true)) {
+ if (in_array((string)$tag->id, $list, true)) {
Log::debug(sprintf('TagList: (id) found tag #%d ("%s") in list.', $tag->id, $tag->tag));
return true;
diff --git a/app/Support/Binder/TagOrId.php b/app/Support/Binder/TagOrId.php
index bc511e5018..e742fb674d 100644
--- a/app/Support/Binder/TagOrId.php
+++ b/app/Support/Binder/TagOrId.php
@@ -42,7 +42,7 @@ class TagOrId implements BinderInterface
$result = $repository->findByTag($value);
if (null === $result) {
- $result = $repository->find((int) $value);
+ $result = $repository->find((int)$value);
}
if (null !== $result) {
return $result;
diff --git a/app/Support/Binder/UserGroupAccount.php b/app/Support/Binder/UserGroupAccount.php
index 12d7eff4a2..c395655e87 100644
--- a/app/Support/Binder/UserGroupAccount.php
+++ b/app/Support/Binder/UserGroupAccount.php
@@ -41,7 +41,7 @@ class UserGroupAccount implements BinderInterface
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
- $account = Account::where('id', (int) $value)
+ $account = Account::where('id', (int)$value)
->where('user_group_id', $user->user_group_id)
->first()
;
diff --git a/app/Support/Binder/UserGroupBill.php b/app/Support/Binder/UserGroupBill.php
index 551846d693..bd2489965e 100644
--- a/app/Support/Binder/UserGroupBill.php
+++ b/app/Support/Binder/UserGroupBill.php
@@ -41,7 +41,7 @@ class UserGroupBill implements BinderInterface
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
- $currency = Bill::where('id', (int) $value)
+ $currency = Bill::where('id', (int)$value)
->where('user_group_id', $user->user_group_id)
->first()
;
diff --git a/app/Support/Binder/UserGroupExchangeRate.php b/app/Support/Binder/UserGroupExchangeRate.php
index 74a65c9348..862564fde1 100644
--- a/app/Support/Binder/UserGroupExchangeRate.php
+++ b/app/Support/Binder/UserGroupExchangeRate.php
@@ -38,7 +38,7 @@ class UserGroupExchangeRate implements BinderInterface
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
- $rate = CurrencyExchangeRate::where('id', (int) $value)
+ $rate = CurrencyExchangeRate::where('id', (int)$value)
->where('user_group_id', $user->user_group_id)
->first()
;
diff --git a/app/Support/Binder/UserGroupTransaction.php b/app/Support/Binder/UserGroupTransaction.php
index d9131400f3..fbbf5c1f43 100644
--- a/app/Support/Binder/UserGroupTransaction.php
+++ b/app/Support/Binder/UserGroupTransaction.php
@@ -38,7 +38,7 @@ class UserGroupTransaction implements BinderInterface
if (auth()->check()) {
/** @var User $user */
$user = auth()->user();
- $group = TransactionGroup::where('id', (int) $value)
+ $group = TransactionGroup::where('id', (int)$value)
->where('user_group_id', $user->user_group_id)
->first()
;
diff --git a/app/Support/CacheProperties.php b/app/Support/CacheProperties.php
index b81f040467..e22808ea06 100644
--- a/app/Support/CacheProperties.php
+++ b/app/Support/CacheProperties.php
@@ -78,6 +78,14 @@ class CacheProperties
return Cache::has($this->hash);
}
+ /**
+ * @param mixed $data
+ */
+ public function store($data): void
+ {
+ Cache::forever($this->hash, $data);
+ }
+
private function hash(): void
{
$content = '';
@@ -86,17 +94,9 @@ class CacheProperties
$content = sprintf('%s%s', $content, json_encode($property, JSON_THROW_ON_ERROR));
} catch (JsonException) {
// @ignoreException
- $content = sprintf('%s%s', $content, hash('sha256', (string) Carbon::now()->getTimestamp()));
+ $content = sprintf('%s%s', $content, hash('sha256', (string)Carbon::now()->getTimestamp()));
}
}
$this->hash = substr(hash('sha256', $content), 0, 16);
}
-
- /**
- * @param mixed $data
- */
- public function store($data): void
- {
- Cache::forever($this->hash, $data);
- }
}
diff --git a/app/Support/Calendar/Calculator.php b/app/Support/Calendar/Calculator.php
index 3dff9dff07..ae5c1d7f72 100644
--- a/app/Support/Calendar/Calculator.php
+++ b/app/Support/Calendar/Calculator.php
@@ -37,27 +37,6 @@ class Calculator
private static ?SplObjectStorage $intervalMap = null; // @phpstan-ignore-line
private static array $intervals = [];
- /**
- * @throws IntervalException
- */
- public function nextDateByInterval(Carbon $epoch, Periodicity $periodicity, int $skipInterval = 0): Carbon
- {
- if (!self::isAvailablePeriodicity($periodicity)) {
- throw IntervalException::unavailable($periodicity, self::$intervals);
- }
-
- /** @var Periodicity\Interval $periodicity */
- $periodicity = self::$intervalMap->offsetGet($periodicity);
- $interval = $this->skipInterval($skipInterval);
-
- return $periodicity->nextDate($epoch->clone(), $interval);
- }
-
- public function isAvailablePeriodicity(Periodicity $periodicity): bool
- {
- return self::containsInterval($periodicity);
- }
-
private static function containsInterval(Periodicity $periodicity): bool
{
return self::loadIntervalMap()->contains($periodicity);
@@ -78,6 +57,27 @@ class Calculator
return self::$intervalMap;
}
+ public function isAvailablePeriodicity(Periodicity $periodicity): bool
+ {
+ return self::containsInterval($periodicity);
+ }
+
+ /**
+ * @throws IntervalException
+ */
+ public function nextDateByInterval(Carbon $epoch, Periodicity $periodicity, int $skipInterval = 0): Carbon
+ {
+ if (!self::isAvailablePeriodicity($periodicity)) {
+ throw IntervalException::unavailable($periodicity, self::$intervals);
+ }
+
+ /** @var Periodicity\Interval $periodicity */
+ $periodicity = self::$intervalMap->offsetGet($periodicity);
+ $interval = $this->skipInterval($skipInterval);
+
+ return $periodicity->nextDate($epoch->clone(), $interval);
+ }
+
private function skipInterval(int $skip): int
{
return self::DEFAULT_INTERVAL + $skip;
diff --git a/app/Support/Chart/Budget/FrontpageChartGenerator.php b/app/Support/Chart/Budget/FrontpageChartGenerator.php
index 9e351afc78..b5af64fedd 100644
--- a/app/Support/Chart/Budget/FrontpageChartGenerator.php
+++ b/app/Support/Chart/Budget/FrontpageChartGenerator.php
@@ -69,9 +69,9 @@ class FrontpageChartGenerator
Log::debug('Now in generate for budget chart.');
$budgets = $this->budgetRepository->getActiveBudgets();
$data = [
- ['label' => (string) trans('firefly.spent_in_budget'), 'entries' => [], 'type' => 'bar'],
- ['label' => (string) trans('firefly.left_to_spend'), 'entries' => [], 'type' => 'bar'],
- ['label' => (string) trans('firefly.overspent'), 'entries' => [], 'type' => 'bar'],
+ ['label' => (string)trans('firefly.spent_in_budget'), 'entries' => [], 'type' => 'bar'],
+ ['label' => (string)trans('firefly.left_to_spend'), 'entries' => [], 'type' => 'bar'],
+ ['label' => (string)trans('firefly.overspent'), 'entries' => [], 'type' => 'bar'],
];
// loop al budgets:
@@ -84,6 +84,64 @@ class FrontpageChartGenerator
return $data;
}
+ public function setEnd(Carbon $end): void
+ {
+ $this->end = $end;
+ }
+
+ public function setStart(Carbon $start): void
+ {
+ $this->start = $start;
+ }
+
+ /**
+ * A basic setter for the user. Also updates the repositories with the right user.
+ */
+ public function setUser(User $user): void
+ {
+ $this->budgetRepository->setUser($user);
+ $this->blRepository->setUser($user);
+ $this->opsRepository->setUser($user);
+
+ $locale = app('steam')->getLocale();
+ $this->monthAndDayFormat = (string)trans('config.month_and_day_js', [], $locale);
+ }
+
+ /**
+ * If a budget has budget limit, each limit is processed individually.
+ */
+ private function budgetLimits(array $data, Budget $budget, Collection $limits): array
+ {
+ Log::debug('Start processing budget limits.');
+
+ /** @var BudgetLimit $limit */
+ foreach ($limits as $limit) {
+ $data = $this->processLimit($data, $budget, $limit);
+ }
+ Log::debug('Done processing budget limits.');
+
+ return $data;
+ }
+
+ /**
+ * When no limits are present, the expenses of the whole period are collected and grouped.
+ * This is grouped per currency. Because there is no limit set, "left to spend" and "overspent" are empty.
+ */
+ private function noBudgetLimits(array $data, Budget $budget): array
+ {
+ $spent = $this->opsRepository->sumExpenses($this->start, $this->end, null, new Collection()->push($budget));
+
+ /** @var array $entry */
+ foreach ($spent as $entry) {
+ $title = sprintf('%s (%s)', $budget->name, $entry['currency_name']);
+ $data[0]['entries'][$title] = bcmul((string)$entry['sum'], '-1'); // spent
+ $data[1]['entries'][$title] = 0; // left to spend
+ $data[2]['entries'][$title] = 0; // overspent
+ }
+
+ return $data;
+ }
+
/**
* For each budget, gets all budget limits for the current time range.
* When no limits are present, the time range is used to collect information on money spent.
@@ -108,41 +166,6 @@ class FrontpageChartGenerator
return $result;
}
- /**
- * When no limits are present, the expenses of the whole period are collected and grouped.
- * This is grouped per currency. Because there is no limit set, "left to spend" and "overspent" are empty.
- */
- private function noBudgetLimits(array $data, Budget $budget): array
- {
- $spent = $this->opsRepository->sumExpenses($this->start, $this->end, null, new Collection()->push($budget));
-
- /** @var array $entry */
- foreach ($spent as $entry) {
- $title = sprintf('%s (%s)', $budget->name, $entry['currency_name']);
- $data[0]['entries'][$title] = bcmul((string) $entry['sum'], '-1'); // spent
- $data[1]['entries'][$title] = 0; // left to spend
- $data[2]['entries'][$title] = 0; // overspent
- }
-
- return $data;
- }
-
- /**
- * If a budget has budget limit, each limit is processed individually.
- */
- private function budgetLimits(array $data, Budget $budget, Collection $limits): array
- {
- Log::debug('Start processing budget limits.');
-
- /** @var BudgetLimit $limit */
- foreach ($limits as $limit) {
- $data = $this->processLimit($data, $budget, $limit);
- }
- Log::debug('Done processing budget limits.');
-
- return $data;
- }
-
/**
* For each limit, the expenses from the time range of the limit are collected. Each row from the result is
* processed individually.
@@ -204,14 +227,14 @@ class FrontpageChartGenerator
Log::debug(sprintf('Amount is now "%s".', $amount));
}
$amount ??= '0';
- $sumSpent = bcmul((string) $entry['sum'], '-1'); // spent
+ $sumSpent = bcmul((string)$entry['sum'], '-1'); // spent
$data[0]['entries'][$title] ??= '0';
$data[1]['entries'][$title] ??= '0';
$data[2]['entries'][$title] ??= '0';
- $data[0]['entries'][$title] = bcadd((string) $data[0]['entries'][$title], 1 === bccomp($sumSpent, $amount) ? $amount : $sumSpent); // spent
- $data[1]['entries'][$title] = bcadd((string) $data[1]['entries'][$title], 1 === bccomp($amount, $sumSpent) ? bcadd((string) $entry['sum'], $amount) : '0'); // left to spent
- $data[2]['entries'][$title] = bcadd((string) $data[2]['entries'][$title], 1 === bccomp($amount, $sumSpent) ? '0' : bcmul(bcadd((string) $entry['sum'], $amount), '-1')); // overspent
+ $data[0]['entries'][$title] = bcadd((string)$data[0]['entries'][$title], 1 === bccomp($sumSpent, $amount) ? $amount : $sumSpent); // spent
+ $data[1]['entries'][$title] = bcadd((string)$data[1]['entries'][$title], 1 === bccomp($amount, $sumSpent) ? bcadd((string)$entry['sum'], $amount) : '0'); // left to spent
+ $data[2]['entries'][$title] = bcadd((string)$data[2]['entries'][$title], 1 === bccomp($amount, $sumSpent) ? '0' : bcmul(bcadd((string)$entry['sum'], $amount), '-1')); // overspent
Log::debug(sprintf('Amount [spent] is now %s.', $data[0]['entries'][$title]));
Log::debug(sprintf('Amount [left] is now %s.', $data[1]['entries'][$title]));
@@ -219,27 +242,4 @@ class FrontpageChartGenerator
return $data;
}
-
- public function setEnd(Carbon $end): void
- {
- $this->end = $end;
- }
-
- public function setStart(Carbon $start): void
- {
- $this->start = $start;
- }
-
- /**
- * A basic setter for the user. Also updates the repositories with the right user.
- */
- public function setUser(User $user): void
- {
- $this->budgetRepository->setUser($user);
- $this->blRepository->setUser($user);
- $this->opsRepository->setUser($user);
-
- $locale = app('steam')->getLocale();
- $this->monthAndDayFormat = (string) trans('config.month_and_day_js', [], $locale);
- }
}
diff --git a/app/Support/Chart/Category/FrontpageChartGenerator.php b/app/Support/Chart/Category/FrontpageChartGenerator.php
index c27f13c686..cc9b249235 100644
--- a/app/Support/Chart/Category/FrontpageChartGenerator.php
+++ b/app/Support/Chart/Category/FrontpageChartGenerator.php
@@ -26,7 +26,6 @@ namespace FireflyIII\Support\Chart\Category;
use Carbon\Carbon;
use FireflyIII\Enums\AccountTypeEnum;
-use FireflyIII\Models\Category;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
@@ -96,6 +95,30 @@ class FrontpageChartGenerator
];
}
+ private function collectExpensesAll(Collection $categories, Collection $accounts): array
+ {
+ Log::debug(sprintf('Collect expenses for %d category(ies).', count($categories)));
+ $spent = $this->opsRepos->collectExpenses($this->start, $this->end, $accounts, $categories);
+ $tempData = [];
+ foreach ($categories as $category) {
+ $sums = $this->opsRepos->sumCollectedTransactionsByCategory($spent, $category, 'negative', $this->convertToPrimary);
+ if (0 === count($sums)) {
+ continue;
+ }
+ foreach ($sums as $currency) {
+ $this->addCurrency($currency);
+ $tempData[] = [
+ 'name' => $category->name,
+ 'sum' => $currency['sum'],
+ 'sum_float' => round((float)$currency['sum'], $currency['currency_decimal_places']),
+ 'currency_id' => (int)$currency['currency_id'],
+ ];
+ }
+ }
+
+ return $tempData;
+ }
+
private function collectNoCatExpenses(Collection $accounts): array
{
$noCatExp = $this->noCatRepos->sumExpenses($this->start, $this->end, $accounts);
@@ -147,28 +170,4 @@ class FrontpageChartGenerator
return $currencyData;
}
-
- private function collectExpensesAll(Collection $categories, Collection $accounts): array
- {
- Log::debug(sprintf('Collect expenses for %d category(ies).', count($categories)));
- $spent = $this->opsRepos->collectExpenses($this->start, $this->end, $accounts, $categories);
- $tempData = [];
- foreach ($categories as $category) {
- $sums = $this->opsRepos->sumCollectedTransactionsByCategory($spent, $category, 'negative', $this->convertToPrimary);
- if (0 === count($sums)) {
- continue;
- }
- foreach ($sums as $currency) {
- $this->addCurrency($currency);
- $tempData[] = [
- 'name' => $category->name,
- 'sum' => $currency['sum'],
- 'sum_float' => round((float)$currency['sum'], $currency['currency_decimal_places']),
- 'currency_id' => (int)$currency['currency_id'],
- ];
- }
- }
-
- return $tempData;
- }
}
diff --git a/app/Support/Chart/Category/WholePeriodChartGenerator.php b/app/Support/Chart/Category/WholePeriodChartGenerator.php
index 2a28cb4d62..ce43b1d16e 100644
--- a/app/Support/Chart/Category/WholePeriodChartGenerator.php
+++ b/app/Support/Chart/Category/WholePeriodChartGenerator.php
@@ -73,14 +73,14 @@ class WholePeriodChartGenerator
$code = $currency['currency_code'];
$name = $currency['currency_name'];
$chartData[sprintf('spent-in-%s', $code)] = [
- 'label' => (string) trans('firefly.box_spent_in_currency', ['currency' => $name]),
+ 'label' => (string)trans('firefly.box_spent_in_currency', ['currency' => $name]),
'entries' => [],
'type' => 'bar',
'backgroundColor' => 'rgba(219, 68, 55, 0.5)', // red
];
$chartData[sprintf('earned-in-%s', $code)] = [
- 'label' => (string) trans('firefly.box_earned_in_currency', ['currency' => $name]),
+ 'label' => (string)trans('firefly.box_earned_in_currency', ['currency' => $name]),
'entries' => [],
'type' => 'bar',
'backgroundColor' => 'rgba(0, 141, 76, 0.5)', // green
diff --git a/app/Support/Chart/ChartData.php b/app/Support/Chart/ChartData.php
index ab03e3dd31..d35242c4d6 100644
--- a/app/Support/Chart/ChartData.php
+++ b/app/Support/Chart/ChartData.php
@@ -44,10 +44,10 @@ class ChartData
public function add(array $data): void
{
if (array_key_exists('currency_id', $data)) {
- $data['currency_id'] = (string) $data['currency_id'];
+ $data['currency_id'] = (string)$data['currency_id'];
}
if (array_key_exists('primary_currency_id', $data)) {
- $data['primary_currency_id'] = (string) $data['primary_currency_id'];
+ $data['primary_currency_id'] = (string)$data['primary_currency_id'];
}
$required = ['start', 'date', 'end', 'entries'];
foreach ($required as $field) {
diff --git a/app/Support/Cronjobs/AutoBudgetCronjob.php b/app/Support/Cronjobs/AutoBudgetCronjob.php
index f71a4a4da1..e4e82d2376 100644
--- a/app/Support/Cronjobs/AutoBudgetCronjob.php
+++ b/app/Support/Cronjobs/AutoBudgetCronjob.php
@@ -39,9 +39,9 @@ class AutoBudgetCronjob extends AbstractCronjob
{
/** @var Configuration $config */
$config = FireflyConfig::get('last_ab_job', 0);
- $lastTime = (int) $config->data;
- $diff = Carbon::now()->getTimestamp() - $lastTime;
- $diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
+ $lastTime = (int)$config->data;
+ $diff = now(config('app.timezone'))->getTimestamp() - $lastTime;
+ $diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
if (0 === $lastTime) {
Log::info('Auto budget cron-job has never fired before.');
}
@@ -80,7 +80,7 @@ class AutoBudgetCronjob extends AbstractCronjob
$this->jobSucceeded = true;
$this->message = 'Auto-budget cron job fired successfully.';
- FireflyConfig::set('last_ab_job', (int) $this->date->format('U'));
+ FireflyConfig::set('last_ab_job', (int)$this->date->format('U'));
Log::info('Done with auto budget cron job task.');
}
}
diff --git a/app/Support/Cronjobs/BillWarningCronjob.php b/app/Support/Cronjobs/BillWarningCronjob.php
index aece4d403f..a358d5879e 100644
--- a/app/Support/Cronjobs/BillWarningCronjob.php
+++ b/app/Support/Cronjobs/BillWarningCronjob.php
@@ -45,9 +45,9 @@ class BillWarningCronjob extends AbstractCronjob
/** @var Configuration $config */
$config = FireflyConfig::get('last_bw_job', 0);
- $lastTime = (int) $config->data;
- $diff = Carbon::now()->getTimestamp() - $lastTime;
- $diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
+ $lastTime = (int)$config->data;
+ $diff = now(config('app.timezone'))->getTimestamp() - $lastTime;
+ $diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
if (0 === $lastTime) {
Log::info('The bill notification cron-job has never fired before.');
@@ -93,8 +93,8 @@ class BillWarningCronjob extends AbstractCronjob
$this->jobSucceeded = true;
$this->message = 'Bill notification cron job fired successfully.';
- FireflyConfig::set('last_bw_job', (int) $this->date->format('U'));
- Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int) $this->date->format('U')));
+ FireflyConfig::set('last_bw_job', (int)$this->date->format('U'));
+ Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int)$this->date->format('U')));
Log::info('Done with bill notification cron job task.');
}
}
diff --git a/app/Support/Cronjobs/ExchangeRatesCronjob.php b/app/Support/Cronjobs/ExchangeRatesCronjob.php
index f7d80b8033..889d6e57de 100644
--- a/app/Support/Cronjobs/ExchangeRatesCronjob.php
+++ b/app/Support/Cronjobs/ExchangeRatesCronjob.php
@@ -39,9 +39,9 @@ class ExchangeRatesCronjob extends AbstractCronjob
{
/** @var Configuration $config */
$config = FireflyConfig::get('last_cer_job', 0);
- $lastTime = (int) $config->data;
- $diff = Carbon::now()->getTimestamp() - $lastTime;
- $diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
+ $lastTime = (int)$config->data;
+ $diff = now(config('app.timezone'))->getTimestamp() - $lastTime;
+ $diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
if (0 === $lastTime) {
Log::info('Exchange rates cron-job has never fired before.');
}
@@ -81,7 +81,7 @@ class ExchangeRatesCronjob extends AbstractCronjob
$this->jobSucceeded = true;
$this->message = 'Exchange rates cron job fired successfully.';
- FireflyConfig::set('last_cer_job', (int) $this->date->format('U'));
+ FireflyConfig::set('last_cer_job', (int)$this->date->format('U'));
Log::info('Done with exchange rates job task.');
}
}
diff --git a/app/Support/Cronjobs/RecurringCronjob.php b/app/Support/Cronjobs/RecurringCronjob.php
index cc6bb7f901..5f4e11a4c2 100644
--- a/app/Support/Cronjobs/RecurringCronjob.php
+++ b/app/Support/Cronjobs/RecurringCronjob.php
@@ -45,9 +45,9 @@ class RecurringCronjob extends AbstractCronjob
/** @var Configuration $config */
$config = FireflyConfig::get('last_rt_job', 0);
- $lastTime = (int) $config->data;
- $diff = Carbon::now()->getTimestamp() - $lastTime;
- $diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
+ $lastTime = (int)$config->data;
+ $diff = now(config('app.timezone'))->getTimestamp() - $lastTime;
+ $diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
if (0 === $lastTime) {
Log::info('Recurring transactions cron-job has never fired before.');
@@ -90,8 +90,8 @@ class RecurringCronjob extends AbstractCronjob
$this->jobSucceeded = true;
$this->message = 'Recurring transactions cron job fired successfully.';
- FireflyConfig::set('last_rt_job', (int) $this->date->format('U'));
- Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int) $this->date->format('U')));
+ FireflyConfig::set('last_rt_job', (int)$this->date->format('U'));
+ Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int)$this->date->format('U')));
Log::info('Done with recurring cron job task.');
}
}
diff --git a/app/Support/Cronjobs/UpdateCheckCronjob.php b/app/Support/Cronjobs/UpdateCheckCronjob.php
index 6d3cea13ab..d9987a73ad 100644
--- a/app/Support/Cronjobs/UpdateCheckCronjob.php
+++ b/app/Support/Cronjobs/UpdateCheckCronjob.php
@@ -42,7 +42,7 @@ class UpdateCheckCronjob extends AbstractCronjob
// should not check for updates:
$permission = FireflyConfig::get('permission_update_check', -1);
- $value = (int) $permission->data;
+ $value = (int)$permission->data;
if (1 !== $value) {
Log::debug('Update check is not enabled.');
// get stuff from job:
diff --git a/app/Support/Cronjobs/WebhookCronjob.php b/app/Support/Cronjobs/WebhookCronjob.php
index dcac057140..84ea6676f5 100644
--- a/app/Support/Cronjobs/WebhookCronjob.php
+++ b/app/Support/Cronjobs/WebhookCronjob.php
@@ -45,9 +45,9 @@ class WebhookCronjob extends AbstractCronjob
/** @var Configuration $config */
$config = FireflyConfig::get('last_webhook_job', 0);
- $lastTime = (int) $config->data;
- $diff = Carbon::now()->getTimestamp() - $lastTime;
- $diffForHumans = today(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
+ $lastTime = (int)$config->data;
+ $diff = now(config('app.timezone'))->getTimestamp() - $lastTime;
+ $diffForHumans = now(config('app.timezone'))->diffForHumans(Carbon::createFromTimestamp($lastTime), null, true);
if (0 === $lastTime) {
Log::info('The webhook cron-job has never fired before.');
@@ -90,8 +90,8 @@ class WebhookCronjob extends AbstractCronjob
$this->jobSucceeded = true;
$this->message = 'Send webhook messages cron job fired successfully.';
- FireflyConfig::set('last_webhook_job', (int) $this->date->format('U'));
- Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int) $this->date->format('U')));
+ FireflyConfig::set('last_webhook_job', (int)$this->date->format('U'));
+ Log::info(sprintf('Marked the last time this job has run as "%s" (%d)', $this->date->format('Y-m-d H:i:s'), (int)$this->date->format('U')));
Log::info('Done with webhook cron job task.');
}
}
diff --git a/app/Support/Debug/Timer.php b/app/Support/Debug/Timer.php
index 94e48187b7..31119addc4 100644
--- a/app/Support/Debug/Timer.php
+++ b/app/Support/Debug/Timer.php
@@ -28,8 +28,8 @@ use Illuminate\Support\Facades\Log;
class Timer
{
- private array $times = [];
private static ?Timer $instance = null;
+ private array $times = [];
private function __construct()
{
@@ -38,7 +38,7 @@ class Timer
public static function getInstance(): self
{
- if (null === self::$instance) {
+ if (!self::$instance instanceof self) {
self::$instance = new self();
}
diff --git a/app/Support/ExpandedForm.php b/app/Support/ExpandedForm.php
index e460353ac2..fc464fae22 100644
--- a/app/Support/ExpandedForm.php
+++ b/app/Support/ExpandedForm.php
@@ -23,9 +23,9 @@ declare(strict_types=1);
namespace FireflyIII\Support;
-use Illuminate\Database\Eloquent\Model;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Support\Form\FormSupport;
+use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Throwable;
diff --git a/app/Support/Export/ExportDataGenerator.php b/app/Support/Export/ExportDataGenerator.php
index d4a44e4e6a..926f371108 100644
--- a/app/Support/Export/ExportDataGenerator.php
+++ b/app/Support/Export/ExportDataGenerator.php
@@ -85,7 +85,7 @@ class ExportDataGenerator
private bool $exportTransactions;
private Carbon $start;
private User $user;
- private UserGroup $userGroup; // @phpstan-ignore-line
+ private UserGroup $userGroup; // @phpstan-ignore-line
public function __construct()
{
@@ -141,6 +141,92 @@ class ExportDataGenerator
return $return;
}
+ /**
+ * @SuppressWarnings("PHPMD.UnusedFormalParameter")
+ */
+ public function get(string $key, mixed $default = null): mixed
+ {
+ return null;
+ }
+
+ /**
+ * @SuppressWarnings("PHPMD.UnusedFormalParameter")
+ */
+ public function has(mixed $key): mixed
+ {
+ return null;
+ }
+
+ public function setAccounts(Collection $accounts): void
+ {
+ $this->accounts = $accounts;
+ }
+
+ public function setEnd(Carbon $end): void
+ {
+ $this->end = $end;
+ }
+
+ public function setExportAccounts(bool $exportAccounts): void
+ {
+ $this->exportAccounts = $exportAccounts;
+ }
+
+ public function setExportBills(bool $exportBills): void
+ {
+ $this->exportBills = $exportBills;
+ }
+
+ public function setExportBudgets(bool $exportBudgets): void
+ {
+ $this->exportBudgets = $exportBudgets;
+ }
+
+ public function setExportCategories(bool $exportCategories): void
+ {
+ $this->exportCategories = $exportCategories;
+ }
+
+ public function setExportPiggies(bool $exportPiggies): void
+ {
+ $this->exportPiggies = $exportPiggies;
+ }
+
+ public function setExportRecurring(bool $exportRecurring): void
+ {
+ $this->exportRecurring = $exportRecurring;
+ }
+
+ public function setExportRules(bool $exportRules): void
+ {
+ $this->exportRules = $exportRules;
+ }
+
+ public function setExportTags(bool $exportTags): void
+ {
+ $this->exportTags = $exportTags;
+ }
+
+ public function setExportTransactions(bool $exportTransactions): void
+ {
+ $this->exportTransactions = $exportTransactions;
+ }
+
+ public function setStart(Carbon $start): void
+ {
+ $this->start = $start;
+ }
+
+ public function setUser(User $user): void
+ {
+ $this->user = $user;
+ }
+
+ public function setUserGroup(UserGroup $userGroup): void
+ {
+ $this->userGroup = $userGroup;
+ }
+
/**
* @throws CannotInsertRecord
* @throws Exception
@@ -222,11 +308,6 @@ class ExportDataGenerator
return $string;
}
- public function setUser(User $user): void
- {
- $this->user = $user;
- }
-
/**
* @throws CannotInsertRecord
* @throws Exception
@@ -588,14 +669,6 @@ class ExportDataGenerator
return $string;
}
- /**
- * @SuppressWarnings("PHPMD.UnusedFormalParameter")
- */
- public function get(string $key, mixed $default = null): mixed
- {
- return null;
- }
-
/**
* @throws CannotInsertRecord
* @throws Exception
@@ -828,11 +901,6 @@ class ExportDataGenerator
return $string;
}
- public function setAccounts(Collection $accounts): void
- {
- $this->accounts = $accounts;
- }
-
private function mergeTags(array $tags): string
{
if (0 === count($tags)) {
@@ -845,72 +913,4 @@ class ExportDataGenerator
return implode(',', $smol);
}
-
- /**
- * @SuppressWarnings("PHPMD.UnusedFormalParameter")
- */
- public function has(mixed $key): mixed
- {
- return null;
- }
-
- public function setEnd(Carbon $end): void
- {
- $this->end = $end;
- }
-
- public function setExportAccounts(bool $exportAccounts): void
- {
- $this->exportAccounts = $exportAccounts;
- }
-
- public function setExportBills(bool $exportBills): void
- {
- $this->exportBills = $exportBills;
- }
-
- public function setExportBudgets(bool $exportBudgets): void
- {
- $this->exportBudgets = $exportBudgets;
- }
-
- public function setExportCategories(bool $exportCategories): void
- {
- $this->exportCategories = $exportCategories;
- }
-
- public function setExportPiggies(bool $exportPiggies): void
- {
- $this->exportPiggies = $exportPiggies;
- }
-
- public function setExportRecurring(bool $exportRecurring): void
- {
- $this->exportRecurring = $exportRecurring;
- }
-
- public function setExportRules(bool $exportRules): void
- {
- $this->exportRules = $exportRules;
- }
-
- public function setExportTags(bool $exportTags): void
- {
- $this->exportTags = $exportTags;
- }
-
- public function setExportTransactions(bool $exportTransactions): void
- {
- $this->exportTransactions = $exportTransactions;
- }
-
- public function setStart(Carbon $start): void
- {
- $this->start = $start;
- }
-
- public function setUserGroup(UserGroup $userGroup): void
- {
- $this->userGroup = $userGroup;
- }
}
diff --git a/app/Support/FireflyConfig.php b/app/Support/FireflyConfig.php
index 499b123602..70d4650c6c 100644
--- a/app/Support/FireflyConfig.php
+++ b/app/Support/FireflyConfig.php
@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace FireflyIII\Support;
+use Exception;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Configuration;
use Illuminate\Contracts\Encryption\DecryptException;
@@ -30,7 +31,6 @@ use Illuminate\Contracts\Encryption\EncryptException;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
-use Exception;
/**
* Class FireflyConfig.
@@ -46,34 +46,6 @@ class FireflyConfig
Configuration::where('name', $name)->forceDelete();
}
- public function has(string $name): bool
- {
- return 1 === Configuration::where('name', $name)->count();
- }
-
- public function getEncrypted(string $name, mixed $default = null): ?Configuration
- {
- $result = $this->get($name, $default);
- if (!$result instanceof Configuration) {
- return null;
- }
- if ('' === $result->data) {
- Log::warning(sprintf('Empty encrypted configuration value found: "%s"', $name));
-
- return $result;
- }
-
- try {
- $result->data = decrypt($result->data);
- } catch (DecryptException $e) {
- Log::error(sprintf('Could not decrypt configuration value "%s": %s', $name, $e->getMessage()));
-
- return $result;
- }
-
- return $result;
- }
-
/**
* @param null|bool|int|string $default
*
@@ -106,6 +78,56 @@ class FireflyConfig
return $this->set($name, $default);
}
+ public function getEncrypted(string $name, mixed $default = null): ?Configuration
+ {
+ $result = $this->get($name, $default);
+ if (!$result instanceof Configuration) {
+ return null;
+ }
+ if ('' === $result->data) {
+ Log::warning(sprintf('Empty encrypted configuration value found: "%s"', $name));
+
+ return $result;
+ }
+
+ try {
+ $result->data = decrypt($result->data);
+ } catch (DecryptException $e) {
+ Log::error(sprintf('Could not decrypt configuration value "%s": %s', $name, $e->getMessage()));
+
+ return $result;
+ }
+
+ return $result;
+ }
+
+ public function getFresh(string $name, mixed $default = null): ?Configuration
+ {
+ $config = Configuration::where('name', $name)->first(['id', 'name', 'data']);
+ if (null !== $config) {
+ return $config;
+ }
+ // no preference found and default is null:
+ if (null === $default) {
+ return null;
+ }
+
+ return $this->set($name, $default);
+ }
+
+ public function has(string $name): bool
+ {
+ return 1 === Configuration::where('name', $name)->count();
+ }
+
+ /**
+ * @param mixed $value
+ */
+ public function put(string $name, $value): Configuration
+ {
+ return $this->set($name, $value);
+ }
+
public function set(string $name, mixed $value): Configuration
{
try {
@@ -135,28 +157,6 @@ class FireflyConfig
return $config;
}
- public function getFresh(string $name, mixed $default = null): ?Configuration
- {
- $config = Configuration::where('name', $name)->first(['id', 'name', 'data']);
- if (null !== $config) {
- return $config;
- }
- // no preference found and default is null:
- if (null === $default) {
- return null;
- }
-
- return $this->set($name, $default);
- }
-
- /**
- * @param mixed $value
- */
- public function put(string $name, $value): Configuration
- {
- return $this->set($name, $value);
- }
-
public function setEncrypted(string $name, mixed $value): Configuration
{
try {
diff --git a/app/Support/Form/AccountForm.php b/app/Support/Form/AccountForm.php
index 19a043c76d..9b72eb285a 100644
--- a/app/Support/Form/AccountForm.php
+++ b/app/Support/Form/AccountForm.php
@@ -51,43 +51,12 @@ class AccountForm
$repository = $this->getAccountRepository();
$grouped = $this->getAccountsGrouped($types, $repository);
$cash = $repository->getCashAccount();
- $key = (string) trans('firefly.cash_account_type');
- $grouped[$key][$cash->id] = sprintf('(%s)', (string) trans('firefly.cash'));
+ $key = (string)trans('firefly.cash_account_type');
+ $grouped[$key][$cash->id] = sprintf('(%s)', (string)trans('firefly.cash'));
return $this->select($name, $grouped, $value, $options);
}
- private function getAccountsGrouped(array $types, ?AccountRepositoryInterface $repository = null): array
- {
- if (!$repository instanceof AccountRepositoryInterface) {
- $repository = $this->getAccountRepository();
- }
- $accountList = $repository->getActiveAccountsByType($types);
- $liabilityTypes = [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::LOAN->value];
- $grouped = [];
-
- /** @var Account $account */
- foreach ($accountList as $account) {
- $role = (string) $repository->getMetaValue($account, 'account_role');
- if (in_array($account->accountType->type, $liabilityTypes, true)) {
- $role = sprintf('l_%s', $account->accountType->type);
- }
- if ('' === $role) {
- $role = 'no_account_type';
- if (AccountTypeEnum::EXPENSE->value === $account->accountType->type) {
- $role = 'expense_account';
- }
- if (AccountTypeEnum::REVENUE->value === $account->accountType->type) {
- $role = 'revenue_account';
- }
- }
- $key = (string) trans(sprintf('firefly.opt_group_%s', $role));
- $grouped[$key][$account->id] = $account->name;
- }
-
- return $grouped;
- }
-
/**
* Grouped dropdown list of all accounts that are valid as the destination of a withdrawal.
*/
@@ -98,8 +67,8 @@ class AccountForm
$grouped = $this->getAccountsGrouped($types, $repository);
$cash = $repository->getCashAccount();
- $key = (string) trans('firefly.cash_account_type');
- $grouped[$key][$cash->id] = sprintf('(%s)', (string) trans('firefly.cash'));
+ $key = (string)trans('firefly.cash_account_type');
+ $grouped[$key][$cash->id] = sprintf('(%s)', (string)trans('firefly.cash'));
return $this->select($name, $grouped, $value, $options);
}
@@ -173,4 +142,35 @@ class AccountForm
return $this->select($name, $grouped, $value, $options);
}
+
+ private function getAccountsGrouped(array $types, ?AccountRepositoryInterface $repository = null): array
+ {
+ if (!$repository instanceof AccountRepositoryInterface) {
+ $repository = $this->getAccountRepository();
+ }
+ $accountList = $repository->getActiveAccountsByType($types);
+ $liabilityTypes = [AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::CREDITCARD->value, AccountTypeEnum::LOAN->value];
+ $grouped = [];
+
+ /** @var Account $account */
+ foreach ($accountList as $account) {
+ $role = (string)$repository->getMetaValue($account, 'account_role');
+ if (in_array($account->accountType->type, $liabilityTypes, true)) {
+ $role = sprintf('l_%s', $account->accountType->type);
+ }
+ if ('' === $role) {
+ $role = 'no_account_type';
+ if (AccountTypeEnum::EXPENSE->value === $account->accountType->type) {
+ $role = 'expense_account';
+ }
+ if (AccountTypeEnum::REVENUE->value === $account->accountType->type) {
+ $role = 'revenue_account';
+ }
+ }
+ $key = (string)trans(sprintf('firefly.opt_group_%s', $role));
+ $grouped[$key][$account->id] = $account->name;
+ }
+
+ return $grouped;
+ }
}
diff --git a/app/Support/Form/CurrencyForm.php b/app/Support/Form/CurrencyForm.php
index 88816667d4..bf748c6097 100644
--- a/app/Support/Form/CurrencyForm.php
+++ b/app/Support/Form/CurrencyForm.php
@@ -49,60 +49,6 @@ class CurrencyForm
return $this->currencyField($name, 'amount', $value, $options);
}
- /**
- * @phpstan-param view-string $view
- *
- * @throws FireflyException
- */
- protected function currencyField(string $name, string $view, mixed $value = null, ?array $options = null): string
- {
- $label = $this->label($name, $options);
- $options = $this->expandOptionArray($name, $label, $options);
- $classes = $this->getHolderClasses($name);
- $value = $this->fillFieldValue($name, $value);
- $options['step'] = 'any';
- $primaryCurrency = $options['currency'] ?? app('amount')->getPrimaryCurrency();
-
- /** @var Collection $currencies */
- $currencies = app('amount')->getCurrencies();
- unset($options['currency'], $options['placeholder']);
- // perhaps the currency has been sent to us in the field $amount_currency_id_$name (amount_currency_id_amount)
- $preFilled = session('preFilled');
- if (!is_array($preFilled)) {
- $preFilled = [];
- }
- $key = 'amount_currency_id_'.$name;
- $sentCurrencyId = array_key_exists($key, $preFilled) ? (int) $preFilled[$key] : $primaryCurrency->id;
-
- app('log')->debug(sprintf('Sent currency ID is %d', $sentCurrencyId));
-
- // find this currency in set of currencies:
- foreach ($currencies as $currency) {
- if ($currency->id === $sentCurrencyId) {
- $primaryCurrency = $currency;
- app('log')->debug(sprintf('default currency is now %s', $primaryCurrency->code));
-
- break;
- }
- }
-
- // make sure value is formatted nicely:
- if (null !== $value && '' !== $value) {
- $value = app('steam')->bcround($value, $primaryCurrency->decimal_places);
- }
-
- try {
- $html = view('form.'.$view, compact('primaryCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render();
- } catch (Throwable $e) {
- app('log')->debug(sprintf('Could not render currencyField(): %s', $e->getMessage()));
- $html = 'Could not render currencyField.';
-
- throw new FireflyException($html, 0, $e);
- }
-
- return $html;
- }
-
/**
* TODO describe and cleanup.
*
@@ -115,63 +61,6 @@ class CurrencyForm
return $this->allCurrencyField($name, 'balance', $value, $options);
}
- /**
- * TODO describe and cleanup
- *
- * @param mixed $value
- *
- * @throws FireflyException
- */
- protected function allCurrencyField(string $name, string $view, $value = null, ?array $options = null): string
- {
- $label = $this->label($name, $options);
- $options = $this->expandOptionArray($name, $label, $options);
- $classes = $this->getHolderClasses($name);
- $value = $this->fillFieldValue($name, $value);
- $options['step'] = 'any';
- $primaryCurrency = $options['currency'] ?? app('amount')->getPrimaryCurrency();
-
- /** @var Collection $currencies */
- $currencies = app('amount')->getAllCurrencies();
- unset($options['currency'], $options['placeholder']);
-
- // perhaps the currency has been sent to us in the field $amount_currency_id_$name (amount_currency_id_amount)
- $preFilled = session('preFilled');
- if (!is_array($preFilled)) {
- $preFilled = [];
- }
- $key = 'amount_currency_id_'.$name;
- $sentCurrencyId = array_key_exists($key, $preFilled) ? (int) $preFilled[$key] : $primaryCurrency->id;
-
- app('log')->debug(sprintf('Sent currency ID is %d', $sentCurrencyId));
-
- // find this currency in set of currencies:
- foreach ($currencies as $currency) {
- if ($currency->id === $sentCurrencyId) {
- $primaryCurrency = $currency;
- app('log')->debug(sprintf('default currency is now %s', $primaryCurrency->code));
-
- break;
- }
- }
-
- // make sure value is formatted nicely:
- if (null !== $value && '' !== $value) {
- $value = app('steam')->bcround($value, $primaryCurrency->decimal_places);
- }
-
- try {
- $html = view('form.'.$view, compact('primaryCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render();
- } catch (Throwable $e) {
- app('log')->debug(sprintf('Could not render currencyField(): %s', $e->getMessage()));
- $html = 'Could not render currencyField.';
-
- throw new FireflyException($html, 0, $e);
- }
-
- return $html;
- }
-
/**
* TODO cleanup and describe
*
@@ -207,7 +96,7 @@ class CurrencyForm
// get all currencies:
$list = $currencyRepos->get();
$array = [
- 0 => (string) trans('firefly.no_currency'),
+ 0 => (string)trans('firefly.no_currency'),
];
/** @var TransactionCurrency $currency */
@@ -217,4 +106,115 @@ class CurrencyForm
return $this->select($name, $array, $value, $options);
}
+
+ /**
+ * TODO describe and cleanup
+ *
+ * @param mixed $value
+ *
+ * @throws FireflyException
+ */
+ protected function allCurrencyField(string $name, string $view, $value = null, ?array $options = null): string
+ {
+ $label = $this->label($name, $options);
+ $options = $this->expandOptionArray($name, $label, $options);
+ $classes = $this->getHolderClasses($name);
+ $value = $this->fillFieldValue($name, $value);
+ $options['step'] = 'any';
+ $primaryCurrency = $options['currency'] ?? app('amount')->getPrimaryCurrency();
+
+ /** @var Collection $currencies */
+ $currencies = app('amount')->getAllCurrencies();
+ unset($options['currency'], $options['placeholder']);
+
+ // perhaps the currency has been sent to us in the field $amount_currency_id_$name (amount_currency_id_amount)
+ $preFilled = session('preFilled');
+ if (!is_array($preFilled)) {
+ $preFilled = [];
+ }
+ $key = 'amount_currency_id_'.$name;
+ $sentCurrencyId = array_key_exists($key, $preFilled) ? (int)$preFilled[$key] : $primaryCurrency->id;
+
+ app('log')->debug(sprintf('Sent currency ID is %d', $sentCurrencyId));
+
+ // find this currency in set of currencies:
+ foreach ($currencies as $currency) {
+ if ($currency->id === $sentCurrencyId) {
+ $primaryCurrency = $currency;
+ app('log')->debug(sprintf('default currency is now %s', $primaryCurrency->code));
+
+ break;
+ }
+ }
+
+ // make sure value is formatted nicely:
+ if (null !== $value && '' !== $value) {
+ $value = app('steam')->bcround($value, $primaryCurrency->decimal_places);
+ }
+
+ try {
+ $html = view('form.'.$view, compact('primaryCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render();
+ } catch (Throwable $e) {
+ app('log')->debug(sprintf('Could not render currencyField(): %s', $e->getMessage()));
+ $html = 'Could not render currencyField.';
+
+ throw new FireflyException($html, 0, $e);
+ }
+
+ return $html;
+ }
+
+ /**
+ * @phpstan-param view-string $view
+ *
+ * @throws FireflyException
+ */
+ protected function currencyField(string $name, string $view, mixed $value = null, ?array $options = null): string
+ {
+ $label = $this->label($name, $options);
+ $options = $this->expandOptionArray($name, $label, $options);
+ $classes = $this->getHolderClasses($name);
+ $value = $this->fillFieldValue($name, $value);
+ $options['step'] = 'any';
+ $primaryCurrency = $options['currency'] ?? app('amount')->getPrimaryCurrency();
+
+ /** @var Collection $currencies */
+ $currencies = app('amount')->getCurrencies();
+ unset($options['currency'], $options['placeholder']);
+ // perhaps the currency has been sent to us in the field $amount_currency_id_$name (amount_currency_id_amount)
+ $preFilled = session('preFilled');
+ if (!is_array($preFilled)) {
+ $preFilled = [];
+ }
+ $key = 'amount_currency_id_'.$name;
+ $sentCurrencyId = array_key_exists($key, $preFilled) ? (int)$preFilled[$key] : $primaryCurrency->id;
+
+ app('log')->debug(sprintf('Sent currency ID is %d', $sentCurrencyId));
+
+ // find this currency in set of currencies:
+ foreach ($currencies as $currency) {
+ if ($currency->id === $sentCurrencyId) {
+ $primaryCurrency = $currency;
+ app('log')->debug(sprintf('default currency is now %s', $primaryCurrency->code));
+
+ break;
+ }
+ }
+
+ // make sure value is formatted nicely:
+ if (null !== $value && '' !== $value) {
+ $value = app('steam')->bcround($value, $primaryCurrency->decimal_places);
+ }
+
+ try {
+ $html = view('form.'.$view, compact('primaryCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render();
+ } catch (Throwable $e) {
+ app('log')->debug(sprintf('Could not render currencyField(): %s', $e->getMessage()));
+ $html = 'Could not render currencyField.';
+
+ throw new FireflyException($html, 0, $e);
+ }
+
+ return $html;
+ }
}
diff --git a/app/Support/Form/FormSupport.php b/app/Support/Form/FormSupport.php
index 4bcc0fcb87..22a580c295 100644
--- a/app/Support/Form/FormSupport.php
+++ b/app/Support/Form/FormSupport.php
@@ -54,15 +54,26 @@ trait FormSupport
return $html;
}
- protected function label(string $name, ?array $options = null): string
+ /**
+ * @param mixed $selected
+ */
+ public function select(string $name, ?array $list = null, $selected = null, ?array $options = null): string
{
- $options ??= [];
- if (array_key_exists('label', $options)) {
- return $options['label'];
- }
- $name = str_replace('[]', '', $name);
+ $list ??= [];
+ $label = $this->label($name, $options);
+ $options = $this->expandOptionArray($name, $label, $options);
+ $classes = $this->getHolderClasses($name);
+ $selected = $this->fillFieldValue($name, $selected);
+ unset($options['autocomplete'], $options['placeholder']);
- return (string)trans('form.'.$name);
+ try {
+ $html = view('form.select', compact('classes', 'name', 'label', 'selected', 'options', 'list'))->render();
+ } catch (Throwable $e) {
+ app('log')->debug(sprintf('Could not render select(): %s', $e->getMessage()));
+ $html = 'Could not render select.';
+ }
+
+ return $html;
}
/**
@@ -80,19 +91,6 @@ trait FormSupport
return $options;
}
- protected function getHolderClasses(string $name): string
- {
- // Get errors from session:
- /** @var null|MessageBag $errors */
- $errors = session('errors');
-
- if (null !== $errors && $errors->has($name)) {
- return 'form-group has-error has-feedback';
- }
-
- return 'form-group';
- }
-
/**
* @param null|mixed $value
*
@@ -116,28 +114,6 @@ trait FormSupport
return $value;
}
- /**
- * @param mixed $selected
- */
- public function select(string $name, ?array $list = null, $selected = null, ?array $options = null): string
- {
- $list ??= [];
- $label = $this->label($name, $options);
- $options = $this->expandOptionArray($name, $label, $options);
- $classes = $this->getHolderClasses($name);
- $selected = $this->fillFieldValue($name, $selected);
- unset($options['autocomplete'], $options['placeholder']);
-
- try {
- $html = view('form.select', compact('classes', 'name', 'label', 'selected', 'options', 'list'))->render();
- } catch (Throwable $e) {
- app('log')->debug(sprintf('Could not render select(): %s', $e->getMessage()));
- $html = 'Could not render select.';
- }
-
- return $html;
- }
-
protected function getAccountRepository(): AccountRepositoryInterface
{
return app(AccountRepositoryInterface::class);
@@ -147,4 +123,28 @@ trait FormSupport
{
return today(config('app.timezone'));
}
+
+ protected function getHolderClasses(string $name): string
+ {
+ // Get errors from session:
+ /** @var null|MessageBag $errors */
+ $errors = session('errors');
+
+ if (null !== $errors && $errors->has($name)) {
+ return 'form-group has-error has-feedback';
+ }
+
+ return 'form-group';
+ }
+
+ protected function label(string $name, ?array $options = null): string
+ {
+ $options ??= [];
+ if (array_key_exists('label', $options)) {
+ return $options['label'];
+ }
+ $name = str_replace('[]', '', $name);
+
+ return (string)trans('form.'.$name);
+ }
}
diff --git a/app/Support/Form/PiggyBankForm.php b/app/Support/Form/PiggyBankForm.php
index 78919b30bf..0818d96157 100644
--- a/app/Support/Form/PiggyBankForm.php
+++ b/app/Support/Form/PiggyBankForm.php
@@ -47,7 +47,7 @@ class PiggyBankForm
/** @var PiggyBankRepositoryInterface $repository */
$repository = app(PiggyBankRepositoryInterface::class);
$piggyBanks = $repository->getPiggyBanksWithAmount();
- $title = (string) trans('firefly.default_group_title_name');
+ $title = (string)trans('firefly.default_group_title_name');
$array = [];
$subList = [
0 => [
@@ -55,7 +55,7 @@ class PiggyBankForm
'title' => $title,
],
'piggies' => [
- (string) trans('firefly.none_in_select_list'),
+ (string)trans('firefly.none_in_select_list'),
],
],
];
diff --git a/app/Support/Form/RuleForm.php b/app/Support/Form/RuleForm.php
index 6baee553be..9566f0301f 100644
--- a/app/Support/Form/RuleForm.php
+++ b/app/Support/Form/RuleForm.php
@@ -66,12 +66,12 @@ class RuleForm
// get all currencies:
$list = $groupRepos->get();
$array = [
- 0 => (string) trans('firefly.none_in_select_list'),
+ 0 => (string)trans('firefly.none_in_select_list'),
];
/** @var RuleGroup $group */
foreach ($list as $group) {
- if (array_key_exists('hidden', $options) && (int) $options['hidden'] !== $group->id) {
+ if (array_key_exists('hidden', $options) && (int)$options['hidden'] !== $group->id) {
$array[$group->id] = $group->title;
}
}
diff --git a/app/Support/Http/Api/AccountBalanceGrouped.php b/app/Support/Http/Api/AccountBalanceGrouped.php
index 839c87bfb0..e335da23eb 100644
--- a/app/Support/Http/Api/AccountBalanceGrouped.php
+++ b/app/Support/Http/Api/AccountBalanceGrouped.php
@@ -44,10 +44,10 @@ class AccountBalanceGrouped
private readonly ExchangeRateConverter $converter;
private array $currencies = [];
private array $data = [];
- private TransactionCurrency $primary;
private Carbon $end;
private array $journals = [];
private string $preferredRange;
+ private TransactionCurrency $primary;
private Carbon $start;
public function __construct()
@@ -146,48 +146,49 @@ class AccountBalanceGrouped
$converter->summarize();
}
- private function processJournal(array $journal): void
+ public function setAccounts(Collection $accounts): void
{
- // format the date according to the period
- $period = $journal['date']->format($this->carbonFormat);
- $currencyId = (int)$journal['currency_id'];
- $currency = $this->findCurrency($currencyId);
-
- // set the array with monetary info, if it does not exist.
- $this->createDefaultDataEntry($journal);
- // set the array (in monetary info) with spent/earned in this $period, if it does not exist.
- $this->createDefaultPeriodEntry($journal);
-
- // is this journal's amount in- our outgoing?
- $key = $this->getDataKey($journal);
- $amount = 'spent' === $key ? Steam::negative($journal['amount']) : Steam::positive($journal['amount']);
-
- // get conversion rate
- $rate = $this->getRate($currency, $journal['date']);
- $amountConverted = bcmul($amount, $rate);
-
- // perhaps transaction already has the foreign amount in the primary currency.
- if ((int)$journal['foreign_currency_id'] === $this->primary->id) {
- $amountConverted = $journal['foreign_amount'] ?? '0';
- $amountConverted = 'earned' === $key ? Steam::positive($amountConverted) : Steam::negative($amountConverted);
- }
-
- // add normal entry
- $this->data[$currencyId][$period][$key] = bcadd((string)$this->data[$currencyId][$period][$key], $amount);
-
- // add converted entry
- $convertedKey = sprintf('pc_%s', $key);
- $this->data[$currencyId][$period][$convertedKey] = bcadd((string)$this->data[$currencyId][$period][$convertedKey], $amountConverted);
+ $this->accountIds = $accounts->pluck('id')->toArray();
}
- private function findCurrency(int $currencyId): TransactionCurrency
+ public function setEnd(Carbon $end): void
{
- if (array_key_exists($currencyId, $this->currencies)) {
- return $this->currencies[$currencyId];
- }
- $this->currencies[$currencyId] = Amount::getTransactionCurrencyById($currencyId);
+ $this->end = $end;
+ }
- return $this->currencies[$currencyId];
+ public function setJournals(array $journals): void
+ {
+ $this->journals = $journals;
+ }
+
+ public function setPreferredRange(string $preferredRange): void
+ {
+ $this->preferredRange = $preferredRange;
+ $this->carbonFormat = Navigation::preferredCarbonFormatByPeriod($preferredRange);
+ }
+
+ public function setPrimary(TransactionCurrency $primary): void
+ {
+ $this->primary = $primary;
+ $primaryCurrencyId = $primary->id;
+ $this->currencies = [$primary->id => $primary]; // currency cache
+ $this->data[$primaryCurrencyId] = [
+ 'currency_id' => (string)$primaryCurrencyId,
+ 'currency_symbol' => $primary->symbol,
+ 'currency_code' => $primary->code,
+ 'currency_name' => $primary->name,
+ 'currency_decimal_places' => $primary->decimal_places,
+ 'primary_currency_id' => (string)$primaryCurrencyId,
+ 'primary_currency_symbol' => $primary->symbol,
+ 'primary_currency_code' => $primary->code,
+ 'primary_currency_name' => $primary->name,
+ 'primary_currency_decimal_places' => $primary->decimal_places,
+ ];
+ }
+
+ public function setStart(Carbon $start): void
+ {
+ $this->start = $start;
}
private function createDefaultDataEntry(array $journal): void
@@ -220,6 +221,16 @@ class AccountBalanceGrouped
];
}
+ private function findCurrency(int $currencyId): TransactionCurrency
+ {
+ if (array_key_exists($currencyId, $this->currencies)) {
+ return $this->currencies[$currencyId];
+ }
+ $this->currencies[$currencyId] = Amount::getTransactionCurrencyById($currencyId);
+
+ return $this->currencies[$currencyId];
+ }
+
private function getDataKey(array $journal): string
{
// deposit = incoming
@@ -254,48 +265,37 @@ class AccountBalanceGrouped
return $rate;
}
- public function setAccounts(Collection $accounts): void
+ private function processJournal(array $journal): void
{
- $this->accountIds = $accounts->pluck('id')->toArray();
- }
+ // format the date according to the period
+ $period = $journal['date']->format($this->carbonFormat);
+ $currencyId = (int)$journal['currency_id'];
+ $currency = $this->findCurrency($currencyId);
- public function setPrimary(TransactionCurrency $primary): void
- {
- $this->primary = $primary;
- $primaryCurrencyId = $primary->id;
- $this->currencies = [$primary->id => $primary]; // currency cache
- $this->data[$primaryCurrencyId] = [
- 'currency_id' => (string)$primaryCurrencyId,
- 'currency_symbol' => $primary->symbol,
- 'currency_code' => $primary->code,
- 'currency_name' => $primary->name,
- 'currency_decimal_places' => $primary->decimal_places,
- 'primary_currency_id' => (string)$primaryCurrencyId,
- 'primary_currency_symbol' => $primary->symbol,
- 'primary_currency_code' => $primary->code,
- 'primary_currency_name' => $primary->name,
- 'primary_currency_decimal_places' => $primary->decimal_places,
- ];
- }
+ // set the array with monetary info, if it does not exist.
+ $this->createDefaultDataEntry($journal);
+ // set the array (in monetary info) with spent/earned in this $period, if it does not exist.
+ $this->createDefaultPeriodEntry($journal);
- public function setEnd(Carbon $end): void
- {
- $this->end = $end;
- }
+ // is this journal's amount in- our outgoing?
+ $key = $this->getDataKey($journal);
+ $amount = 'spent' === $key ? Steam::negative($journal['amount']) : Steam::positive($journal['amount']);
- public function setJournals(array $journals): void
- {
- $this->journals = $journals;
- }
+ // get conversion rate
+ $rate = $this->getRate($currency, $journal['date']);
+ $amountConverted = bcmul($amount, $rate);
- public function setPreferredRange(string $preferredRange): void
- {
- $this->preferredRange = $preferredRange;
- $this->carbonFormat = Navigation::preferredCarbonFormatByPeriod($preferredRange);
- }
+ // perhaps transaction already has the foreign amount in the primary currency.
+ if ((int)$journal['foreign_currency_id'] === $this->primary->id) {
+ $amountConverted = $journal['foreign_amount'] ?? '0';
+ $amountConverted = 'earned' === $key ? Steam::positive($amountConverted) : Steam::negative($amountConverted);
+ }
- public function setStart(Carbon $start): void
- {
- $this->start = $start;
+ // add normal entry
+ $this->data[$currencyId][$period][$key] = bcadd((string)$this->data[$currencyId][$period][$key], $amount);
+
+ // add converted entry
+ $convertedKey = sprintf('pc_%s', $key);
+ $this->data[$currencyId][$period][$convertedKey] = bcadd((string)$this->data[$currencyId][$period][$convertedKey], $amountConverted);
}
}
diff --git a/app/Support/Http/Api/CollectsAccountsFromFilter.php b/app/Support/Http/Api/CollectsAccountsFromFilter.php
index 7c10a6c1e1..2911ed61f6 100644
--- a/app/Support/Http/Api/CollectsAccountsFromFilter.php
+++ b/app/Support/Http/Api/CollectsAccountsFromFilter.php
@@ -39,7 +39,7 @@ trait CollectsAccountsFromFilter
// always collect from the query parameter, even when it's empty.
if (null !== $queryParameters['accounts']) {
foreach ($queryParameters['accounts'] as $accountId) {
- $account = $this->repository->find((int) $accountId);
+ $account = $this->repository->find((int)$accountId);
if (null !== $account) {
$collection->push($account);
}
diff --git a/app/Support/Http/Api/ExchangeRateConverter.php b/app/Support/Http/Api/ExchangeRateConverter.php
index 8abb75fe76..d92313907a 100644
--- a/app/Support/Http/Api/ExchangeRateConverter.php
+++ b/app/Support/Http/Api/ExchangeRateConverter.php
@@ -94,6 +94,149 @@ class ExchangeRateConverter
return '0' === $rate ? '1' : $rate;
}
+ public function setIgnoreSettings(bool $ignoreSettings): void
+ {
+ $this->ignoreSettings = $ignoreSettings;
+ }
+
+ public function setUserGroup(UserGroup $userGroup): void
+ {
+ $this->userGroup = $userGroup;
+ }
+
+ public function summarize(): void
+ {
+ if (false === $this->enabled()) {
+ return;
+ }
+ Log::debug(sprintf('ExchangeRateConverter ran %d queries.', $this->queryCount));
+ }
+
+ private function getCacheKey(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string
+ {
+ return sprintf('cer-%d-%d-%s', $from->id, $to->id, $date->format('Y-m-d'));
+ }
+
+ /**
+ * @throws FireflyException
+ */
+ private function getEuroId(): int
+ {
+ Log::debug('getEuroId()');
+ $cache = new CacheProperties();
+ $cache->addProperty('cer-euro-id');
+ if ($cache->has()) {
+ return (int)$cache->get();
+ }
+ $euro = Amount::getTransactionCurrencyByCode('EUR');
+ ++$this->queryCount;
+ $cache->store($euro->id);
+
+ return $euro->id;
+ }
+
+ /**
+ * @throws FireflyException
+ */
+ private function getEuroRate(TransactionCurrency $currency, Carbon $date): string
+ {
+ $euroId = $this->getEuroId();
+ if ($euroId === $currency->id) {
+ return '1';
+ }
+ $rate = $this->getFromDB($currency->id, $euroId, $date->format('Y-m-d'));
+
+ if (null !== $rate) {
+ // app('log')->debug(sprintf('Rate for %s to EUR is %s.', $currency->code, $rate));
+ return $rate;
+ }
+ $rate = $this->getFromDB($euroId, $currency->id, $date->format('Y-m-d'));
+ if (null !== $rate) {
+ return bcdiv('1', $rate);
+ // app('log')->debug(sprintf('Inverted rate for %s to EUR is %s.', $currency->code, $rate));
+ // return $rate;
+ }
+ // grab backup values from config file:
+ $backup = config(sprintf('cer.rates.%s', $currency->code));
+ if (null !== $backup) {
+ return bcdiv('1', (string)$backup);
+ // app('log')->debug(sprintf('Backup rate for %s to EUR is %s.', $currency->code, $backup));
+ // return $backup;
+ }
+
+ // app('log')->debug(sprintf('No rate for %s to EUR.', $currency->code));
+ return '0';
+ }
+
+ private function getFromDB(int $from, int $to, string $date): ?string
+ {
+ if ($from === $to) {
+ Log::debug('ExchangeRateConverter: From and to are the same, return "1".');
+
+ return '1';
+ }
+ $key = sprintf('cer-%d-%d-%s', $from, $to, $date);
+
+ // perhaps the rate has been cached during this particular run
+ $preparedRate = $this->prepared[$date][$from][$to] ?? null;
+ if (null !== $preparedRate && 0 !== bccomp('0', $preparedRate)) {
+ Log::debug(sprintf('ExchangeRateConverter: Found prepared rate from #%d to #%d on %s.', $from, $to, $date));
+
+ return $preparedRate;
+ }
+
+ $cache = new CacheProperties();
+ $cache->addProperty($key);
+ if ($cache->has()) {
+ $rate = $cache->get();
+ if ('' === $rate) {
+ return null;
+ }
+ Log::debug(sprintf('ExchangeRateConverter: Found cached rate from #%d to #%d on %s.', $from, $to, $date));
+
+ return $rate;
+ }
+
+ /** @var null|CurrencyExchangeRate $result */
+ $result = $this->userGroup->currencyExchangeRates()
+ ->where('from_currency_id', $from)
+ ->where('to_currency_id', $to)
+ ->where('date', '<=', $date)
+ ->orderBy('date', 'DESC')
+ ->first()
+ ;
+ ++$this->queryCount;
+ $rate = (string)$result?->rate;
+
+ if ('' === $rate) {
+ app('log')->debug(sprintf('ExchangeRateConverter: Found no rate for #%d->#%d (%s) in the DB.', $from, $to, $date));
+
+ return null;
+ }
+ if (0 === bccomp('0', $rate)) {
+ app('log')->debug(sprintf('ExchangeRateConverter: Found rate for #%d->#%d (%s) in the DB, but it\'s zero.', $from, $to, $date));
+
+ return null;
+ }
+ app('log')->debug(sprintf('ExchangeRateConverter: Found rate for #%d->#%d (%s) in the DB: %s.', $from, $to, $date, $rate));
+ $cache->store($rate);
+
+ // if the rate has not been cached during this particular run, save it
+ $this->prepared[$date] ??= [
+ $from => [
+ $to => $rate,
+ ],
+ ];
+ // also save the exchange rate the other way around:
+ $this->prepared[$date] ??= [
+ $to => [
+ $from => bcdiv('1', $rate),
+ ],
+ ];
+
+ return $rate;
+ }
+
/**
* @throws FireflyException
*/
@@ -146,147 +289,4 @@ class ExchangeRateConverter
return $rate;
}
-
- private function getCacheKey(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): string
- {
- return sprintf('cer-%d-%d-%s', $from->id, $to->id, $date->format('Y-m-d'));
- }
-
- private function getFromDB(int $from, int $to, string $date): ?string
- {
- if ($from === $to) {
- Log::debug('ExchangeRateConverter: From and to are the same, return "1".');
-
- return '1';
- }
- $key = sprintf('cer-%d-%d-%s', $from, $to, $date);
-
- // perhaps the rate has been cached during this particular run
- $preparedRate = $this->prepared[$date][$from][$to] ?? null;
- if (null !== $preparedRate && 0 !== bccomp('0', $preparedRate)) {
- Log::debug(sprintf('ExchangeRateConverter: Found prepared rate from #%d to #%d on %s.', $from, $to, $date));
-
- return $preparedRate;
- }
-
- $cache = new CacheProperties();
- $cache->addProperty($key);
- if ($cache->has()) {
- $rate = $cache->get();
- if ('' === $rate) {
- return null;
- }
- Log::debug(sprintf('ExchangeRateConverter: Found cached rate from #%d to #%d on %s.', $from, $to, $date));
-
- return $rate;
- }
-
- /** @var null|CurrencyExchangeRate $result */
- $result = $this->userGroup->currencyExchangeRates()
- ->where('from_currency_id', $from)
- ->where('to_currency_id', $to)
- ->where('date', '<=', $date)
- ->orderBy('date', 'DESC')
- ->first()
- ;
- ++$this->queryCount;
- $rate = (string) $result?->rate;
-
- if ('' === $rate) {
- app('log')->debug(sprintf('ExchangeRateConverter: Found no rate for #%d->#%d (%s) in the DB.', $from, $to, $date));
-
- return null;
- }
- if (0 === bccomp('0', $rate)) {
- app('log')->debug(sprintf('ExchangeRateConverter: Found rate for #%d->#%d (%s) in the DB, but it\'s zero.', $from, $to, $date));
-
- return null;
- }
- app('log')->debug(sprintf('ExchangeRateConverter: Found rate for #%d->#%d (%s) in the DB: %s.', $from, $to, $date, $rate));
- $cache->store($rate);
-
- // if the rate has not been cached during this particular run, save it
- $this->prepared[$date] ??= [
- $from => [
- $to => $rate,
- ],
- ];
- // also save the exchange rate the other way around:
- $this->prepared[$date] ??= [
- $to => [
- $from => bcdiv('1', $rate),
- ],
- ];
-
- return $rate;
- }
-
- /**
- * @throws FireflyException
- */
- private function getEuroRate(TransactionCurrency $currency, Carbon $date): string
- {
- $euroId = $this->getEuroId();
- if ($euroId === $currency->id) {
- return '1';
- }
- $rate = $this->getFromDB($currency->id, $euroId, $date->format('Y-m-d'));
-
- if (null !== $rate) {
- // app('log')->debug(sprintf('Rate for %s to EUR is %s.', $currency->code, $rate));
- return $rate;
- }
- $rate = $this->getFromDB($euroId, $currency->id, $date->format('Y-m-d'));
- if (null !== $rate) {
- return bcdiv('1', $rate);
- // app('log')->debug(sprintf('Inverted rate for %s to EUR is %s.', $currency->code, $rate));
- // return $rate;
- }
- // grab backup values from config file:
- $backup = config(sprintf('cer.rates.%s', $currency->code));
- if (null !== $backup) {
- return bcdiv('1', (string) $backup);
- // app('log')->debug(sprintf('Backup rate for %s to EUR is %s.', $currency->code, $backup));
- // return $backup;
- }
-
- // app('log')->debug(sprintf('No rate for %s to EUR.', $currency->code));
- return '0';
- }
-
- /**
- * @throws FireflyException
- */
- private function getEuroId(): int
- {
- Log::debug('getEuroId()');
- $cache = new CacheProperties();
- $cache->addProperty('cer-euro-id');
- if ($cache->has()) {
- return (int) $cache->get();
- }
- $euro = Amount::getTransactionCurrencyByCode('EUR');
- ++$this->queryCount;
- $cache->store($euro->id);
-
- return $euro->id;
- }
-
- public function setIgnoreSettings(bool $ignoreSettings): void
- {
- $this->ignoreSettings = $ignoreSettings;
- }
-
- public function setUserGroup(UserGroup $userGroup): void
- {
- $this->userGroup = $userGroup;
- }
-
- public function summarize(): void
- {
- if (false === $this->enabled()) {
- return;
- }
- Log::debug(sprintf('ExchangeRateConverter ran %d queries.', $this->queryCount));
- }
}
diff --git a/app/Support/Http/Api/SummaryBalanceGrouped.php b/app/Support/Http/Api/SummaryBalanceGrouped.php
index b50ab2160e..e4fdb41b68 100644
--- a/app/Support/Http/Api/SummaryBalanceGrouped.php
+++ b/app/Support/Http/Api/SummaryBalanceGrouped.php
@@ -60,7 +60,7 @@ class SummaryBalanceGrouped
$return[] = [
'key' => sprintf('%s-in-pc', $title),
'value' => $this->amounts[$key]['primary'] ?? '0',
- 'currency_id' => (string) $this->default->id,
+ 'currency_id' => (string)$this->default->id,
'currency_code' => $this->default->code,
'currency_symbol' => $this->default->symbol,
'currency_decimal_places' => $this->default->decimal_places,
@@ -73,7 +73,7 @@ class SummaryBalanceGrouped
// skip primary entries.
continue;
}
- $currencyId = (int) $currencyId;
+ $currencyId = (int)$currencyId;
$currency = $this->currencies[$currencyId] ?? $this->currencyRepository->find($currencyId);
$this->currencies[$currencyId] = $currency;
// create objects for big array.
@@ -87,7 +87,7 @@ class SummaryBalanceGrouped
$return[] = [
'key' => sprintf('%s-in-%s', $title, $currency->code),
'value' => $this->amounts[$key][$currencyId] ?? '0',
- 'currency_id' => (string) $currency->id,
+ 'currency_id' => (string)$currency->id,
'currency_code' => $currency->code,
'currency_symbol' => $currency->symbol,
'currency_decimal_places' => $currency->decimal_places,
@@ -109,12 +109,12 @@ class SummaryBalanceGrouped
/** @var array $journal */
foreach ($journals as $journal) {
// transaction info:
- $currencyId = (int) $journal['currency_id'];
- $amount = bcmul((string) $journal['amount'], $multiplier);
+ $currencyId = (int)$journal['currency_id'];
+ $amount = bcmul((string)$journal['amount'], $multiplier);
$currency = $this->currencies[$currencyId] ?? Amount::getTransactionCurrencyById($currencyId);
$this->currencies[$currencyId] = $currency;
$pcAmount = $converter->convert($currency, $this->default, $journal['date'], $amount);
- if ((int) $journal['foreign_currency_id'] === $this->default->id) {
+ if ((int)$journal['foreign_currency_id'] === $this->default->id) {
// use foreign amount instead
$pcAmount = $journal['foreign_amount'];
}
@@ -126,10 +126,10 @@ class SummaryBalanceGrouped
$this->amounts[self::SUM]['primary'] ??= '0';
// add values:
- $this->amounts[$key][$currencyId] = bcadd((string) $this->amounts[$key][$currencyId], $amount);
- $this->amounts[self::SUM][$currencyId] = bcadd((string) $this->amounts[self::SUM][$currencyId], $amount);
- $this->amounts[$key]['primary'] = bcadd((string) $this->amounts[$key]['primary'], (string) $pcAmount);
- $this->amounts[self::SUM]['primary'] = bcadd((string) $this->amounts[self::SUM]['primary'], (string) $pcAmount);
+ $this->amounts[$key][$currencyId] = bcadd((string)$this->amounts[$key][$currencyId], $amount);
+ $this->amounts[self::SUM][$currencyId] = bcadd((string)$this->amounts[self::SUM][$currencyId], $amount);
+ $this->amounts[$key]['primary'] = bcadd((string)$this->amounts[$key]['primary'], (string)$pcAmount);
+ $this->amounts[self::SUM]['primary'] = bcadd((string)$this->amounts[self::SUM]['primary'], (string)$pcAmount);
}
$converter->summarize();
}
diff --git a/app/Support/Http/Api/ValidatesUserGroupTrait.php b/app/Support/Http/Api/ValidatesUserGroupTrait.php
index 6ce611396d..3d17a7b42c 100644
--- a/app/Support/Http/Api/ValidatesUserGroupTrait.php
+++ b/app/Support/Http/Api/ValidatesUserGroupTrait.php
@@ -38,8 +38,8 @@ use Illuminate\Support\Facades\Log;
*/
trait ValidatesUserGroupTrait
{
+ protected User $user;
protected UserGroup $userGroup;
- protected User $user;
/**
* An "undocumented" filter
@@ -62,11 +62,11 @@ trait ValidatesUserGroupTrait
$user = auth()->user();
$groupId = 0;
if (!$request->has('user_group_id')) {
- $groupId = (int) $user->user_group_id;
+ $groupId = (int)$user->user_group_id;
Log::debug(sprintf('validateUserGroup: no user group submitted, use default group #%d.', $groupId));
}
if ($request->has('user_group_id')) {
- $groupId = (int) $request->get('user_group_id');
+ $groupId = (int)$request->get('user_group_id');
Log::debug(sprintf('validateUserGroup: user group submitted, search for memberships in group #%d.', $groupId));
}
@@ -78,7 +78,7 @@ trait ValidatesUserGroupTrait
if (0 === $memberships->count()) {
Log::debug(sprintf('validateUserGroup: user has no access to group #%d.', $groupId));
- throw new AuthorizationException((string) trans('validation.no_access_group'));
+ throw new AuthorizationException((string)trans('validation.no_access_group'));
}
// need to get the group from the membership:
@@ -86,14 +86,14 @@ trait ValidatesUserGroupTrait
if (null === $group) {
Log::debug(sprintf('validateUserGroup: group #%d does not exist.', $groupId));
- throw new AuthorizationException((string) trans('validation.belongs_user_or_user_group'));
+ throw new AuthorizationException((string)trans('validation.belongs_user_or_user_group'));
}
Log::debug(sprintf('validateUserGroup: validate access of user to group #%d ("%s").', $groupId, $group->title));
$roles = property_exists($this, 'acceptedRoles') ? $this->acceptedRoles : []; // @phpstan-ignore-line
if (0 === count($roles)) {
Log::debug('validateUserGroup: no roles defined, so no access.');
- throw new AuthorizationException((string) trans('validation.no_accepted_roles_defined'));
+ throw new AuthorizationException((string)trans('validation.no_accepted_roles_defined'));
}
Log::debug(sprintf('validateUserGroup: have %d roles to check.', count($roles)), $roles);
@@ -111,6 +111,6 @@ trait ValidatesUserGroupTrait
Log::debug('validateUserGroup: User does NOT have enough rights to access endpoint.');
- throw new AuthorizationException((string) trans('validation.belongs_user_or_user_group'));
+ throw new AuthorizationException((string)trans('validation.belongs_user_or_user_group'));
}
}
diff --git a/app/Support/Http/Controllers/AugumentData.php b/app/Support/Http/Controllers/AugumentData.php
index d882ce2b5b..2046b7e5b2 100644
--- a/app/Support/Http/Controllers/AugumentData.php
+++ b/app/Support/Http/Controllers/AugumentData.php
@@ -110,8 +110,8 @@ trait AugumentData
$grouped = $accounts->groupBy('id')->toArray();
$return = [];
foreach ($accountIds as $combinedId) {
- $parts = explode('-', (string) $combinedId);
- $accountId = (int) $parts[0];
+ $parts = explode('-', (string)$combinedId);
+ $accountId = (int)$parts[0];
if (array_key_exists($accountId, $grouped)) {
$return[$accountId] = $grouped[$accountId][0]['name'];
}
@@ -136,7 +136,7 @@ trait AugumentData
$return[$budgetId] = $grouped[$budgetId][0]['name'];
}
}
- $return[0] = (string) trans('firefly.no_budget');
+ $return[0] = (string)trans('firefly.no_budget');
return $return;
}
@@ -152,13 +152,13 @@ trait AugumentData
$grouped = $categories->groupBy('id')->toArray();
$return = [];
foreach ($categoryIds as $combinedId) {
- $parts = explode('-', (string) $combinedId);
- $categoryId = (int) $parts[0];
+ $parts = explode('-', (string)$combinedId);
+ $categoryId = (int)$parts[0];
if (array_key_exists($categoryId, $grouped)) {
$return[$categoryId] = $grouped[$categoryId][0]['name'];
}
}
- $return[0] = (string) trans('firefly.no_category');
+ $return[0] = (string)trans('firefly.no_category');
return $return;
}
@@ -249,7 +249,7 @@ trait AugumentData
}
$grouped[$name] ??= '0';
- $grouped[$name] = bcadd((string) $journal['amount'], $grouped[$name]);
+ $grouped[$name] = bcadd((string)$journal['amount'], $grouped[$name]);
}
return $grouped;
@@ -272,7 +272,7 @@ trait AugumentData
];
// loop to support multi currency
foreach ($journals as $journal) {
- $currencyId = (int) $journal['currency_id'];
+ $currencyId = (int)$journal['currency_id'];
// if not set, set to zero:
if (!array_key_exists($currencyId, $sum['per_currency'])) {
@@ -287,8 +287,8 @@ trait AugumentData
}
// add amount
- $sum['per_currency'][$currencyId]['sum'] = bcadd($sum['per_currency'][$currencyId]['sum'], (string) $journal['amount']);
- $sum['grand_sum'] = bcadd($sum['grand_sum'], (string) $journal['amount']);
+ $sum['per_currency'][$currencyId]['sum'] = bcadd($sum['per_currency'][$currencyId]['sum'], (string)$journal['amount']);
+ $sum['grand_sum'] = bcadd($sum['grand_sum'], (string)$journal['amount']);
}
return $sum;
diff --git a/app/Support/Http/Controllers/ChartGeneration.php b/app/Support/Http/Controllers/ChartGeneration.php
index 47a85c5c83..c117c51171 100644
--- a/app/Support/Http/Controllers/ChartGeneration.php
+++ b/app/Support/Http/Controllers/ChartGeneration.php
@@ -92,7 +92,7 @@ trait ChartGeneration
Log::debug(sprintf('Start balance for account #%d ("%s) is', $account->id, $account->name), $previous);
while ($currentStart <= $end) {
$format = $currentStart->format('Y-m-d');
- $label = trim($currentStart->isoFormat((string) trans('config.month_and_day_js', [], $locale)));
+ $label = trim($currentStart->isoFormat((string)trans('config.month_and_day_js', [], $locale)));
$balance = $range[$format] ?? $previous;
$previous = $balance;
$currentStart->addDay();
diff --git a/app/Support/Http/Controllers/CreateStuff.php b/app/Support/Http/Controllers/CreateStuff.php
index 6ce33985d3..a69c44ac54 100644
--- a/app/Support/Http/Controllers/CreateStuff.php
+++ b/app/Support/Http/Controllers/CreateStuff.php
@@ -73,7 +73,7 @@ trait CreateStuff
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$assetAccount = [
- 'name' => (string) trans('firefly.cash_wallet', [], $language),
+ 'name' => (string)trans('firefly.cash_wallet', [], $language),
'iban' => null,
'account_type_name' => 'asset',
'virtual_balance' => 0,
@@ -108,7 +108,7 @@ trait CreateStuff
Log::alert('NO OAuth keys were found. They have been created.');
- file_put_contents($publicKey, (string) $key->getPublicKey());
+ file_put_contents($publicKey, (string)$key->getPublicKey());
file_put_contents($privateKey, $key->toString('PKCS1'));
}
@@ -120,7 +120,7 @@ trait CreateStuff
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$savingsAccount = [
- 'name' => (string) trans('firefly.new_savings_account', ['bank_name' => $request->get('bank_name')], $language),
+ 'name' => (string)trans('firefly.new_savings_account', ['bank_name' => $request->get('bank_name')], $language),
'iban' => null,
'account_type_name' => 'asset',
'account_type_id' => null,
diff --git a/app/Support/Http/Controllers/CronRunner.php b/app/Support/Http/Controllers/CronRunner.php
index c8cf03cfd6..ed3e950024 100644
--- a/app/Support/Http/Controllers/CronRunner.php
+++ b/app/Support/Http/Controllers/CronRunner.php
@@ -63,32 +63,6 @@ trait CronRunner
];
}
- protected function webhookCronJob(bool $force, Carbon $date): array
- {
- /** @var WebhookCronjob $webhook */
- $webhook = app(WebhookCronjob::class);
- $webhook->setForce($force);
- $webhook->setDate($date);
-
- try {
- $webhook->fire();
- } catch (FireflyException $e) {
- return [
- 'job_fired' => false,
- 'job_succeeded' => false,
- 'job_errored' => true,
- 'message' => $e->getMessage(),
- ];
- }
-
- return [
- 'job_fired' => $webhook->jobFired,
- 'job_succeeded' => $webhook->jobSucceeded,
- 'job_errored' => $webhook->jobErrored,
- 'message' => $webhook->message,
- ];
- }
-
protected function exchangeRatesCronJob(bool $force, Carbon $date): array
{
/** @var ExchangeRatesCronjob $exchangeRates */
@@ -166,4 +140,30 @@ trait CronRunner
'message' => $recurring->message,
];
}
+
+ protected function webhookCronJob(bool $force, Carbon $date): array
+ {
+ /** @var WebhookCronjob $webhook */
+ $webhook = app(WebhookCronjob::class);
+ $webhook->setForce($force);
+ $webhook->setDate($date);
+
+ try {
+ $webhook->fire();
+ } catch (FireflyException $e) {
+ return [
+ 'job_fired' => false,
+ 'job_succeeded' => false,
+ 'job_errored' => true,
+ 'message' => $e->getMessage(),
+ ];
+ }
+
+ return [
+ 'job_fired' => $webhook->jobFired,
+ 'job_succeeded' => $webhook->jobSucceeded,
+ 'job_errored' => $webhook->jobErrored,
+ 'message' => $webhook->message,
+ ];
+ }
}
diff --git a/app/Support/Http/Controllers/DateCalculation.php b/app/Support/Http/Controllers/DateCalculation.php
index 5027a4c69f..69c30c77d3 100644
--- a/app/Support/Http/Controllers/DateCalculation.php
+++ b/app/Support/Http/Controllers/DateCalculation.php
@@ -40,13 +40,13 @@ trait DateCalculation
*/
public function activeDaysLeft(Carbon $start, Carbon $end): int
{
- $difference = (int) ($start->diffInDays($end, true) + 1);
+ $difference = (int)($start->diffInDays($end, true) + 1);
$today = today(config('app.timezone'))->startOfDay();
if ($start->lte($today) && $end->gte($today)) {
$difference = $today->diffInDays($end) + 1;
}
- return (int) (0 === $difference ? 1 : $difference);
+ return (int)(0 === $difference ? 1 : $difference);
}
/**
@@ -63,7 +63,7 @@ trait DateCalculation
$difference = $start->diffInDays($today, true) + 1;
}
- return (int) $difference;
+ return (int)$difference;
}
protected function calculateStep(Carbon $start, Carbon $end): string
diff --git a/app/Support/Http/Controllers/GetConfigurationData.php b/app/Support/Http/Controllers/GetConfigurationData.php
index 52ce494418..b0bb16b3f1 100644
--- a/app/Support/Http/Controllers/GetConfigurationData.php
+++ b/app/Support/Http/Controllers/GetConfigurationData.php
@@ -48,7 +48,7 @@ trait GetConfigurationData
E_COMPILE_ERROR | E_RECOVERABLE_ERROR | E_ERROR | E_CORE_ERROR => 'E_COMPILE_ERROR|E_RECOVERABLE_ERROR|E_ERROR|E_CORE_ERROR',
];
- return $array[$value] ?? (string) $value;
+ return $array[$value] ?? (string)$value;
}
/**
@@ -64,7 +64,7 @@ trait GetConfigurationData
$currentStep = $options;
// get the text:
- $currentStep['intro'] = (string) trans('intro.'.$route.'_'.$key);
+ $currentStep['intro'] = (string)trans('intro.'.$route.'_'.$key);
// save in array:
$steps[] = $currentStep;
@@ -133,41 +133,41 @@ trait GetConfigurationData
$todayEnd = app('navigation')->endOfPeriod($todayStart, $viewRange);
if ($todayStart->ne($start) || $todayEnd->ne($end)) {
- $ranges[ucfirst((string) trans('firefly.today'))] = [$todayStart, $todayEnd];
+ $ranges[ucfirst((string)trans('firefly.today'))] = [$todayStart, $todayEnd];
}
// last seven days:
$seven = today(config('app.timezone'))->subDays(7);
- $index = (string) trans('firefly.last_seven_days');
+ $index = (string)trans('firefly.last_seven_days');
$ranges[$index] = [$seven, new Carbon()];
// last 30 days:
$thirty = today(config('app.timezone'))->subDays(30);
- $index = (string) trans('firefly.last_thirty_days');
+ $index = (string)trans('firefly.last_thirty_days');
$ranges[$index] = [$thirty, new Carbon()];
// month to date:
$monthBegin = today(config('app.timezone'))->startOfMonth();
- $index = (string) trans('firefly.month_to_date');
+ $index = (string)trans('firefly.month_to_date');
$ranges[$index] = [$monthBegin, new Carbon()];
// year to date:
$yearBegin = today(config('app.timezone'))->startOfYear();
- $index = (string) trans('firefly.year_to_date');
+ $index = (string)trans('firefly.year_to_date');
$ranges[$index] = [$yearBegin, new Carbon()];
// everything
- $index = (string) trans('firefly.everything');
+ $index = (string)trans('firefly.everything');
$ranges[$index] = [$first, new Carbon()];
return [
'title' => $title,
'configuration' => [
- 'apply' => (string) trans('firefly.apply'),
- 'cancel' => (string) trans('firefly.cancel'),
- 'from' => (string) trans('firefly.from'),
- 'to' => (string) trans('firefly.to'),
- 'customRange' => (string) trans('firefly.customRange'),
+ 'apply' => (string)trans('firefly.apply'),
+ 'cancel' => (string)trans('firefly.cancel'),
+ 'from' => (string)trans('firefly.from'),
+ 'to' => (string)trans('firefly.to'),
+ 'customRange' => (string)trans('firefly.customRange'),
'start' => $start->format('Y-m-d'),
'end' => $end->format('Y-m-d'),
'ranges' => $ranges,
@@ -192,7 +192,7 @@ trait GetConfigurationData
$currentStep = $options;
// get the text:
- $currentStep['intro'] = (string) trans('intro.'.$route.'_'.$specificPage.'_'.$key);
+ $currentStep['intro'] = (string)trans('intro.'.$route.'_'.$specificPage.'_'.$key);
// save in array:
$steps[] = $currentStep;
@@ -207,7 +207,7 @@ trait GetConfigurationData
protected function verifyRecurringCronJob(): void
{
$config = FireflyConfig::get('last_rt_job', 0);
- $lastTime = (int) $config?->data;
+ $lastTime = (int)$config?->data;
$now = Carbon::now()->getTimestamp();
app('log')->debug(sprintf('verifyRecurringCronJob: last time is %d ("%s"), now is %d', $lastTime, $config?->data, $now));
if (0 === $lastTime) {
diff --git a/app/Support/Http/Controllers/ModelInformation.php b/app/Support/Http/Controllers/ModelInformation.php
index 65fd660211..093120cf7a 100644
--- a/app/Support/Http/Controllers/ModelInformation.php
+++ b/app/Support/Http/Controllers/ModelInformation.php
@@ -87,9 +87,9 @@ trait ModelInformation
/** @var AccountType $mortgage */
$mortgage = $repository->getAccountTypeByType(AccountTypeEnum::MORTGAGE->value);
$liabilityTypes = [
- $debt->id => (string) trans(sprintf('firefly.account_type_%s', AccountTypeEnum::DEBT->value)),
- $loan->id => (string) trans(sprintf('firefly.account_type_%s', AccountTypeEnum::LOAN->value)),
- $mortgage->id => (string) trans(sprintf('firefly.account_type_%s', AccountTypeEnum::MORTGAGE->value)),
+ $debt->id => (string)trans(sprintf('firefly.account_type_%s', AccountTypeEnum::DEBT->value)),
+ $loan->id => (string)trans(sprintf('firefly.account_type_%s', AccountTypeEnum::LOAN->value)),
+ $mortgage->id => (string)trans(sprintf('firefly.account_type_%s', AccountTypeEnum::MORTGAGE->value)),
];
asort($liabilityTypes);
@@ -100,7 +100,7 @@ trait ModelInformation
{
$roles = [];
foreach (config('firefly.accountRoles') as $role) {
- $roles[$role] = (string) trans(sprintf('firefly.account_role_%s', $role));
+ $roles[$role] = (string)trans(sprintf('firefly.account_role_%s', $role));
}
return $roles;
@@ -118,7 +118,7 @@ trait ModelInformation
$triggers = [];
foreach ($operators as $key => $operator) {
if ('user_action' !== $key && false === $operator['alias']) {
- $triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key));
+ $triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key));
}
}
asort($triggers);
@@ -169,7 +169,7 @@ trait ModelInformation
$triggers = [];
foreach ($operators as $key => $operator) {
if ('user_action' !== $key && false === $operator['alias']) {
- $triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key));
+ $triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key));
}
}
asort($triggers);
diff --git a/app/Support/Http/Controllers/PeriodOverview.php b/app/Support/Http/Controllers/PeriodOverview.php
index afbc198ce2..edb51c7ee3 100644
--- a/app/Support/Http/Controllers/PeriodOverview.php
+++ b/app/Support/Http/Controllers/PeriodOverview.php
@@ -30,13 +30,21 @@ use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\Account;
use FireflyIII\Models\Category;
+use FireflyIII\Models\PeriodStatistic;
use FireflyIII\Models\Tag;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
+use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
+use FireflyIII\Repositories\PeriodStatistic\PeriodStatisticRepositoryInterface;
+use FireflyIII\Repositories\Tag\TagRepositoryInterface;
use FireflyIII\Support\CacheProperties;
-use FireflyIII\Support\Debug\Timer;
+use FireflyIII\Support\Facades\Amount;
use FireflyIII\Support\Facades\Navigation;
+use FireflyIII\Support\Facades\Steam;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Str;
/**
* Trait PeriodOverview.
@@ -67,8 +75,13 @@ use Illuminate\Support\Facades\Log;
*/
trait PeriodOverview
{
- protected AccountRepositoryInterface $accountRepository;
- protected JournalRepositoryInterface $journalRepos;
+ protected AccountRepositoryInterface $accountRepository;
+ protected CategoryRepositoryInterface $categoryRepository;
+ protected TagRepositoryInterface $tagRepository;
+ protected JournalRepositoryInterface $journalRepos;
+ protected PeriodStatisticRepositoryInterface $periodStatisticRepo;
+ private Collection $statistics; // temp data holder
+ private array $transactions; // temp data holder
/**
* This method returns "period entries", so nov-2015, dec-2015, etc. (this depends on the users session range)
@@ -79,130 +92,540 @@ trait PeriodOverview
*/
protected function getAccountPeriodOverview(Account $account, Carbon $start, Carbon $end): array
{
- Log::debug('Now in getAccountPeriodOverview()');
- $timer = Timer::getInstance();
- $timer->start('account-period-total');
+ Log::debug(sprintf('Now in getAccountPeriodOverview(#%d, %s %s)', $account->id, $start->format('Y-m-d H:i:s.u'), $end->format('Y-m-d H:i:s.u')));
$this->accountRepository = app(AccountRepositoryInterface::class);
- $range = Navigation::getViewRange(true);
- [$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
-
- // properties for cache
- $cache = new CacheProperties();
- $cache->addProperty($start);
- $cache->addProperty($end);
- $cache->addProperty('account-show-period-entries');
- $cache->addProperty($account->id);
- if ($cache->has()) {
- Log::debug('Return CACHED in getAccountPeriodOverview()');
-
- return $cache->get();
- }
+ $this->accountRepository->setUser($account->user);
+ $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class);
+ $range = Navigation::getViewRange(true);
+ [$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
/** @var array $dates */
- $dates = Navigation::blockPeriods($start, $end, $range);
- $entries = [];
+ $dates = Navigation::blockPeriods($start, $end, $range);
+ [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end);
+ $this->statistics = $this->periodStatisticRepo->allInRangeForModel($account, $start, $end);
- // run a custom query because doing this with the collector is MEGA slow.
- $timer->start('account-period-collect');
- $transactions = $this->accountRepository->periodCollection($account, $start, $end);
- $timer->stop('account-period-collect');
- // loop dates
+ $entries = [];
Log::debug(sprintf('Count of loops: %d', count($dates)));
- $loops = 0;
- // stop after 10 loops for memory reasons.
- $timer->start('account-period-loop');
foreach ($dates as $currentDate) {
- $title = Navigation::periodShow($currentDate['start'], $currentDate['period']);
- [$transactions, $spent] = $this->filterTransactionsByType(TransactionTypeEnum::WITHDRAWAL, $transactions, $currentDate['start'], $currentDate['end']);
- [$transactions, $earned] = $this->filterTransactionsByType(TransactionTypeEnum::DEPOSIT, $transactions, $currentDate['start'], $currentDate['end']);
- [$transactions, $transferredAway] = $this->filterTransfers('away', $transactions, $currentDate['start'], $currentDate['end']);
- [$transactions, $transferredIn] = $this->filterTransfers('in', $transactions, $currentDate['start'], $currentDate['end']);
- $entries[]
- = [
- 'title' => $title,
- 'route' => route('accounts.show', [$account->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
- 'total_transactions' => count($spent) + count($earned) + count($transferredAway) + count($transferredIn),
- 'spent' => $this->groupByCurrency($spent),
- 'earned' => $this->groupByCurrency($earned),
- 'transferred_away' => $this->groupByCurrency($transferredAway),
- 'transferred_in' => $this->groupByCurrency($transferredIn),
- ];
- ++$loops;
+ $entries[] = $this->getSingleModelPeriod($account, $currentDate['period'], $currentDate['start'], $currentDate['end']);
}
- $timer->stop('account-period-loop');
- $cache->store($entries);
- $timer->stop('account-period-total');
Log::debug('End of getAccountPeriodOverview()');
return $entries;
}
- private function filterTransactionsByType(TransactionTypeEnum $type, array $transactions, Carbon $start, Carbon $end): array
+ private function getPeriodFromBlocks(array $dates, Carbon $start, Carbon $end): array
{
- $result = [];
- $filtered = [];
+ Log::debug('Filter generated periods to select the oldest and newest date.');
+ foreach ($dates as $row) {
+ $currentStart = clone $row['start'];
+ $currentEnd = clone $row['end'];
+ if ($currentStart->lt($start)) {
+ Log::debug(sprintf('New start: was %s, now %s', $start->format('Y-m-d'), $currentStart->format('Y-m-d')));
+ $start = $currentStart;
+ }
+ if ($currentEnd->gt($end)) {
+ Log::debug(sprintf('New end: was %s, now %s', $end->format('Y-m-d'), $currentEnd->format('Y-m-d')));
+ $end = $currentEnd;
+ }
+ }
+
+ return [$start, $end];
+ }
+
+ /**
+ * Overview for single category. Has been refactored recently.
+ *
+ * @throws FireflyException
+ */
+ protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array
+ {
+ $this->categoryRepository = app(CategoryRepositoryInterface::class);
+ $this->categoryRepository->setUser($category->user);
+ $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class);
+
+ $range = Navigation::getViewRange(true);
+ [$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
+
+ /** @var array $dates */
+ $dates = Navigation::blockPeriods($start, $end, $range);
+ $entries = [];
+ [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end);
+ $this->statistics = $this->periodStatisticRepo->allInRangeForModel($category, $start, $end);
+
+
+ Log::debug(sprintf('Count of loops: %d', count($dates)));
+ foreach ($dates as $currentDate) {
+ $entries[] = $this->getSingleModelPeriod($category, $currentDate['period'], $currentDate['start'], $currentDate['end']);
+ }
+
+ return $entries;
+ }
+
+ /**
+ * Same as above, but for lists that involve transactions without a budget.
+ *
+ * This method has been refactored recently.
+ *
+ * @throws FireflyException
+ */
+ protected function getNoModelPeriodOverview(string $model, Carbon $start, Carbon $end): array
+ {
+ Log::debug(sprintf('Now in getNoModelPeriodOverview(%s, %s %s)', $model, $start->format('Y-m-d'), $end->format('Y-m-d')));
+ $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class);
+ $range = Navigation::getViewRange(true);
+ [$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
+
+ /** @var array $dates */
+ $dates = Navigation::blockPeriods($start, $end, $range);
+ [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end);
+ $entries = [];
+ $this->statistics = $this->periodStatisticRepo->allInRangeForPrefix(sprintf('no_%s', $model), $start, $end);
+ Log::debug(sprintf('Collected %d stats', $this->statistics->count()));
+
+ foreach ($dates as $currentDate) {
+ $entries[] = $this->getSingleNoModelPeriodOverview($model, $currentDate['start'], $currentDate['end'], $currentDate['period']);
+ }
+
+ return $entries;
+ }
+
+ private function getSingleNoModelPeriodOverview(string $model, Carbon $start, Carbon $end, string $period): array
+ {
+ Log::debug(sprintf('getSingleNoModelPeriodOverview(%s, %s, %s, %s)', $model, $start->format('Y-m-d'), $end->format('Y-m-d'), $period));
+ $statistics = $this->filterPrefixedStatistics($start, $end, sprintf('no_%s', $model));
+ $title = Navigation::periodShow($end, $period);
+
+ if (0 === $statistics->count()) {
+ Log::debug(sprintf('Found no statistics in period %s - %s, regenerating them.', $start->format('Y-m-d'), $end->format('Y-m-d')));
+ switch ($model) {
+ default:
+ throw new FireflyException(sprintf('Cannot deal with model of type "%s"', $model));
+ case 'budget':
+ // get all expenses without a budget.
+ /** @var GroupCollectorInterface $collector */
+ $collector = app(GroupCollectorInterface::class);
+ $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
+ $spent = $collector->getExtractedJournals();
+ $earned = [];
+ $transferred = [];
+ break;
+ case 'category':
+ // collect all expenses in this period:
+ /** @var GroupCollectorInterface $collector */
+ $collector = app(GroupCollectorInterface::class);
+ $collector->withoutCategory();
+ $collector->setRange($start, $end);
+ $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]);
+ $earned = $collector->getExtractedJournals();
+
+ // collect all income in this period:
+ /** @var GroupCollectorInterface $collector */
+ $collector = app(GroupCollectorInterface::class);
+ $collector->withoutCategory();
+ $collector->setRange($start, $end);
+ $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
+ $spent = $collector->getExtractedJournals();
+
+ // collect all transfers in this period:
+ /** @var GroupCollectorInterface $collector */
+ $collector = app(GroupCollectorInterface::class);
+ $collector->withoutCategory();
+ $collector->setRange($start, $end);
+ $collector->setTypes([TransactionTypeEnum::TRANSFER->value]);
+ $transferred = $collector->getExtractedJournals();
+ break;
+ }
+ $groupedSpent = $this->groupByCurrency($spent);
+ $groupedEarned = $this->groupByCurrency($earned);
+ $groupedTransferred = $this->groupByCurrency($transferred);
+ $entry
+ = [
+ 'title' => $title,
+ 'route' => route(sprintf('%s.no-%s', Str::plural($model), $model), [$start->format('Y-m-d'), $end->format('Y-m-d')]),
+ 'total_transactions' => count($spent),
+ 'spent' => $groupedSpent,
+ 'earned' => $groupedEarned,
+ 'transferred' => $groupedTransferred,
+ ];
+ $this->saveGroupedForPrefix(sprintf('no_%s', $model), $start, $end, 'spent', $groupedSpent);
+ $this->saveGroupedForPrefix(sprintf('no_%s', $model), $start, $end, 'earned', $groupedEarned);
+ $this->saveGroupedForPrefix(sprintf('no_%s', $model), $start, $end, 'transferred', $groupedTransferred);
+ return $entry;
+ }
+ Log::debug(sprintf('Found %d statistics in period %s - %s.', count($statistics), $start->format('Y-m-d'), $end->format('Y-m-d')));
+
+ $entry
+ = [
+ 'title' => $title,
+ 'route' => route(sprintf('%s.no-%s', Str::plural($model), $model), [$start->format('Y-m-d'), $end->format('Y-m-d')]),
+ 'total_transactions' => 0,
+ 'spent' => [],
+ 'earned' => [],
+ 'transferred' => [],
+ ];
+ $grouped = [];
+ /** @var PeriodStatistic $statistic */
+ foreach ($statistics as $statistic) {
+ $type = str_replace(sprintf('no_%s_', $model), '', $statistic->type);
+ $id = (int)$statistic->transaction_currency_id;
+ $currency = Amount::getTransactionCurrencyById($id);
+ $grouped[$type]['count'] ??= 0;
+ $grouped[$type][$id] = [
+ 'amount' => (string)$statistic->amount,
+ 'count' => (int)$statistic->count,
+ 'currency_id' => $currency->id,
+ 'currency_name' => $currency->name,
+ 'currency_code' => $currency->code,
+ 'currency_symbol' => $currency->symbol,
+ 'currency_decimal_places' => $currency->decimal_places,
+ ];
+ $grouped[$type]['count'] += (int)$statistic->count;
+ }
+ $types = ['spent', 'earned', 'transferred'];
+ foreach ($types as $type) {
+ if (array_key_exists($type, $grouped)) {
+ $entry['total_transactions'] += $grouped[$type]['count'];
+ unset($grouped[$type]['count']);
+ $entry[$type] = $grouped[$type];
+ }
+
+ }
+ return $entry;
+ }
+
+ protected function getSingleModelPeriod(Model $model, string $period, Carbon $start, Carbon $end): array
+ {
+ Log::debug(sprintf('Now in getSingleModelPeriod(%s #%d, %s %s)', $model::class, $model->id, $start->format('Y-m-d'), $end->format('Y-m-d')));
+ $types = ['spent', 'earned', 'transferred_in', 'transferred_away'];
+ $return = [
+ 'title' => Navigation::periodShow($start, $period),
+ 'route' => route(sprintf('%s.show', strtolower(Str::plural(class_basename($model)))), [$model->id, $start->format('Y-m-d'), $end->format('Y-m-d')]),
+ 'total_transactions' => 0,
+ ];
+ $this->transactions = [];
+ foreach ($types as $type) {
+ $set = $this->getSingleModelPeriodByType($model, $start, $end, $type);
+ $return['total_transactions'] += $set['count'];
+ unset($set['count']);
+ $return[$type] = $set;
+ }
+
+ return $return;
+ }
+
+
+ private function filterStatistics(Carbon $start, Carbon $end, string $type): Collection
+ {
+ if (0 === $this->statistics->count()) {
+ Log::warning('Have no statistic to filter!');
+ return new Collection;
+ }
+ return $this->statistics->filter(
+ function (PeriodStatistic $statistic) use ($start, $end, $type) {
+ return $statistic->start->eq($start) && $statistic->end->eq($end) && $statistic->type === $type;
+ }
+ );
+ }
+
+ private function filterPrefixedStatistics(Carbon $start, Carbon $end, string $prefix): Collection
+ {
+ if (0 === $this->statistics->count()) {
+ Log::warning('Have no statistic to filter!');
+ return new Collection;
+ }
+ return $this->statistics->filter(
+ function (PeriodStatistic $statistic) use ($start, $end, $prefix) {
+ return $statistic->start->eq($start) && $statistic->end->eq($end) && str_starts_with($statistic->type, $prefix);
+ }
+ );
+ }
+
+
+ private function getSingleModelPeriodByType(Model $model, Carbon $start, Carbon $end, string $type): array
+ {
+ Log::debug(sprintf('Now in getSingleModelPeriodByType(%s #%d, %s %s, %s)', $model::class, $model->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type));
+ $statistics = $this->filterStatistics($start, $end, $type);
+
+ // nothing found, regenerate them.
+ if (0 === $statistics->count()) {
+ Log::debug(sprintf('Found nothing in this period for type "%s"', $type));
+ if (0 === count($this->transactions)) {
+ switch ($model::class) {
+ default:
+ throw new FireflyException(sprintf('Cannot deal with model of type "%s"', $model::class));
+ case Category::class:
+ $this->transactions = $this->categoryRepository->periodCollection($model, $start, $end);
+ break;
+ case Account::class:
+ $this->transactions = $this->accountRepository->periodCollection($model, $start, $end);
+ break;
+ case Tag::class:
+ $this->transactions = $this->tagRepository->periodCollection($model, $start, $end);
+ break;
+ }
+ }
+
+ switch ($type) {
+ default:
+ throw new FireflyException(sprintf('Cannot deal with category period type %s', $type));
+
+ case 'spent':
+
+ $result = $this->filterTransactionsByType(TransactionTypeEnum::WITHDRAWAL, $start, $end);
+
+ break;
+
+ case 'earned':
+ $result = $this->filterTransactionsByType(TransactionTypeEnum::DEPOSIT, $start, $end);
+
+ break;
+
+ case 'transferred_in':
+ $result = $this->filterTransfers('in', $start, $end);
+
+ break;
+
+ case 'transferred_away':
+ $result = $this->filterTransfers('away', $start, $end);
+
+ break;
+ }
+ // each result must be grouped by currency, then saved as period statistic.
+ Log::debug(sprintf('Going to group %d found journal(s)', count($result)));
+ $grouped = $this->groupByCurrency($result);
+
+ $this->saveGroupedAsStatistics($model, $start, $end, $type, $grouped);
+
+ return $grouped;
+ }
+ $grouped = [
+ 'count' => 0,
+ ];
+
+ /** @var PeriodStatistic $statistic */
+ foreach ($statistics as $statistic) {
+ $id = (int)$statistic->transaction_currency_id;
+ $currency = Amount::getTransactionCurrencyById($id);
+ $grouped[$id] = [
+ 'amount' => (string)$statistic->amount,
+ 'count' => (int)$statistic->count,
+ 'currency_id' => $currency->id,
+ 'currency_name' => $currency->name,
+ 'currency_code' => $currency->code,
+ 'currency_symbol' => $currency->symbol,
+ 'currency_decimal_places' => $currency->decimal_places,
+ ];
+ $grouped['count'] += (int)$statistic->count;
+ }
+
+ return $grouped;
+ }
+
+
+ /**
+ * This shows a period overview for a tag. It goes back in time and lists all relevant transactions and sums.
+ *
+ * @throws FireflyException
+ */
+ protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array // period overview for tags.
+ {
+ $this->tagRepository = app(TagRepositoryInterface::class);
+ $this->tagRepository->setUser($tag->user);
+ $this->periodStatisticRepo = app(PeriodStatisticRepositoryInterface::class);
+
+ $range = Navigation::getViewRange(true);
+ [$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
+
+ /** @var array $dates */
+ $dates = Navigation::blockPeriods($start, $end, $range);
+ $entries = [];
+ [$start, $end] = $this->getPeriodFromBlocks($dates, $start, $end);
+ $this->statistics = $this->periodStatisticRepo->allInRangeForModel($tag, $start, $end);
+
+
+ Log::debug(sprintf('Count of loops: %d', count($dates)));
+ foreach ($dates as $currentDate) {
+ $entries[] = $this->getSingleModelPeriod($tag, $currentDate['period'], $currentDate['start'], $currentDate['end']);
+ }
+
+ return $entries;
+ }
+
+ /**
+ * @throws FireflyException
+ */
+ protected function getTransactionPeriodOverview(string $transactionType, Carbon $start, Carbon $end): array
+ {
+ $range = Navigation::getViewRange(true);
+ $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType));
+ [$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
+
+ // properties for cache
+ $cache = new CacheProperties();
+ $cache->addProperty($start);
+ $cache->addProperty($end);
+ $cache->addProperty('transactions-period-entries');
+ $cache->addProperty($transactionType);
+ if ($cache->has()) {
+ return $cache->get();
+ }
+
+ /** @var array $dates */
+ $dates = Navigation::blockPeriods($start, $end, $range);
+ $entries = [];
+ $spent = [];
+ $earned = [];
+ $transferred = [];
+ // collect all journals in this period (regardless of type)
+ $collector = app(GroupCollectorInterface::class);
+ $collector->setTypes($types)->setRange($start, $end);
+ $genericSet = $collector->getExtractedJournals();
+ $loops = 0;
+
+ foreach ($dates as $currentDate) {
+ $title = Navigation::periodShow($currentDate['end'], $currentDate['period']);
+
+ if ($loops < 10) {
+ // set to correct array
+ if ('expenses' === $transactionType || 'withdrawal' === $transactionType) {
+ $spent = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']);
+ }
+ if ('revenue' === $transactionType || 'deposit' === $transactionType) {
+ $earned = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']);
+ }
+ if ('transfer' === $transactionType || 'transfers' === $transactionType) {
+ $transferred = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']);
+ }
+ }
+ $entries[]
+ = [
+ 'title' => $title,
+ 'route' => route('transactions.index', [$transactionType, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
+ 'total_transactions' => count($spent) + count($earned) + count($transferred),
+ 'spent' => $this->groupByCurrency($spent),
+ 'earned' => $this->groupByCurrency($earned),
+ 'transferred' => $this->groupByCurrency($transferred),
+ ];
+ ++$loops;
+ }
+
+ return $entries;
+ }
+
+ private function saveGroupedAsStatistics(Model $model, Carbon $start, Carbon $end, string $type, array $array): void
+ {
+ unset($array['count']);
+ Log::debug(sprintf('saveGroupedAsStatistics(%s #%d, %s, %s, "%s", array(%d))', $model::class, $model->id, $start->format('Y-m-d'), $end->format('Y-m-d'), $type, count($array)));
+ foreach ($array as $entry) {
+ $this->periodStatisticRepo->saveStatistic($model, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']);
+ }
+ if (0 === count($array)) {
+ Log::debug('Save empty statistic.');
+ $this->periodStatisticRepo->saveStatistic($model, $this->primaryCurrency->id, $start, $end, $type, 0, '0');
+ }
+ }
+
+ private function saveGroupedForPrefix(string $prefix, Carbon $start, Carbon $end, string $type, array $array): void
+ {
+ unset($array['count']);
+ Log::debug(sprintf('saveGroupedForPrefix("%s", %s, %s, "%s", array(%d))', $prefix, $start->format('Y-m-d'), $end->format('Y-m-d'), $type, count($array)));
+ foreach ($array as $entry) {
+ $this->periodStatisticRepo->savePrefixedStatistic($prefix, $entry['currency_id'], $start, $end, $type, $entry['count'], $entry['amount']);
+ }
+ if (0 === count($array)) {
+ Log::debug('Save empty statistic.');
+ $this->periodStatisticRepo->savePrefixedStatistic($prefix, $this->primaryCurrency->id, $start, $end, $type, 0, '0');
+ }
+ }
+
+ /**
+ * Filter a list of journals by a set of dates, and then group them by currency.
+ */
+ private function filterJournalsByDate(array $array, Carbon $start, Carbon $end): array
+ {
+ $result = [];
+
+ /** @var array $journal */
+ foreach ($array as $journal) {
+ if ($journal['date'] <= $end && $journal['date'] >= $start) {
+ $result[] = $journal;
+ }
+ }
+
+ return $result;
+ }
+
+
+ private function filterTransactionsByType(TransactionTypeEnum $type, Carbon $start, Carbon $end): array
+ {
+ $result = [];
/**
* @var int $index
* @var array $item
*/
- foreach ($transactions as $index => $item) {
+ foreach ($this->transactions as $item) {
$date = Carbon::parse($item['date']);
$fits = $item['type'] === $type->value && $date >= $start && $date <= $end;
if ($fits) {
+
+ // if type is withdrawal, negative amount:
+ if (TransactionTypeEnum::WITHDRAWAL === $type && 1 === bccomp((string)$item['amount'], '0')) {
+ $item['amount'] = Steam::negative($item['amount']);
+ }
$result[] = $item;
- unset($transactions[$index]);
- }
- if (!$fits) {
- $filtered[] = $item;
}
}
- return [$filtered, $result];
+ return $result;
}
- private function filterTransfers(string $direction, array $transactions, Carbon $start, Carbon $end): array
+
+ private function filterTransfers(string $direction, Carbon $start, Carbon $end): array
{
- $result = [];
- $filtered = [];
+ $result = [];
/**
* @var int $index
* @var array $item
*/
- foreach ($transactions as $index => $item) {
- $date = Carbon::parse($item['date']);
+ foreach ($this->transactions as $item) {
+ $date = Carbon::parse($item['date']);
if ($date >= $start && $date <= $end) {
- if ('away' === $direction && -1 === bccomp((string)$item['amount'], '0')) {
+ if ('Transfer' === $item['type'] && 'away' === $direction && -1 === bccomp((string)$item['amount'], '0')) {
$result[] = $item;
continue;
}
- if ('in' === $direction && 1 === bccomp((string)$item['amount'], '0')) {
+ if ('Transfer' === $item['type'] && 'in' === $direction && 1 === bccomp((string)$item['amount'], '0')) {
$result[] = $item;
-
- continue;
}
}
- $filtered[] = $item;
}
- return [$filtered, $result];
+ return $result;
}
private function groupByCurrency(array $journals): array
{
- $return = [];
+ Log::debug('groupByCurrency()');
+ $return = [
+ 'count' => 0,
+ ];
+ if (0 === count($journals)) {
+ return $return;
+ }
/** @var array $journal */
foreach ($journals as $journal) {
- $currencyId = (int)$journal['currency_id'];
- $currencyCode = $journal['currency_code'];
- $currencyName = $journal['currency_name'];
- $currencySymbol = $journal['currency_symbol'];
- $currencyDecimalPlaces = $journal['currency_decimal_places'];
- $foreignCurrencyId = $journal['foreign_currency_id'];
- $amount = $journal['amount'] ?? '0';
+ $currencyId = (int)$journal['currency_id'];
+ $currencyCode = $journal['currency_code'];
+ $currencyName = $journal['currency_name'];
+ $currencySymbol = $journal['currency_symbol'];
+ $currencyDecimalPlaces = $journal['currency_decimal_places'];
+ $foreignCurrencyId = $journal['foreign_currency_id'];
+ $amount = $journal['amount'] ?? '0';
if ($this->convertToPrimary && $currencyId !== $this->primaryCurrency->id && $foreignCurrencyId !== $this->primaryCurrency->id) {
$amount = $journal['pc_amount'] ?? '0';
@@ -231,410 +654,9 @@ trait PeriodOverview
];
- $return[$currencyId]['amount'] = bcadd($return[$currencyId]['amount'], $amount);
+ $return[$currencyId]['amount'] = bcadd((string)$return[$currencyId]['amount'], $amount);
++$return[$currencyId]['count'];
- }
-
- return $return;
- }
-
- /**
- * Overview for single category. Has been refactored recently.
- *
- * @throws FireflyException
- */
- protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array
- {
- $range = Navigation::getViewRange(true);
- [$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
-
- // properties for entries with their amounts.
- $cache = new CacheProperties();
- $cache->addProperty($start);
- $cache->addProperty($end);
- $cache->addProperty($range);
- $cache->addProperty('category-show-period-entries');
- $cache->addProperty($category->id);
-
- if ($cache->has()) {
- return $cache->get();
- }
-
- /** @var array $dates */
- $dates = Navigation::blockPeriods($start, $end, $range);
- $entries = [];
-
- // collect all expenses in this period:
- /** @var GroupCollectorInterface $collector */
- $collector = app(GroupCollectorInterface::class);
- $collector->setCategory($category);
- $collector->setRange($start, $end);
- $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]);
- $earnedSet = $collector->getExtractedJournals();
-
- // collect all income in this period:
- /** @var GroupCollectorInterface $collector */
- $collector = app(GroupCollectorInterface::class);
- $collector->setCategory($category);
- $collector->setRange($start, $end);
- $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
- $spentSet = $collector->getExtractedJournals();
-
- // collect all transfers in this period:
- /** @var GroupCollectorInterface $collector */
- $collector = app(GroupCollectorInterface::class);
- $collector->setCategory($category);
- $collector->setRange($start, $end);
- $collector->setTypes([TransactionTypeEnum::TRANSFER->value]);
- $transferSet = $collector->getExtractedJournals();
- foreach ($dates as $currentDate) {
- $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']);
- $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']);
- $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']);
- $title = Navigation::periodShow($currentDate['end'], $currentDate['period']);
- $entries[]
- = [
- 'transactions' => 0,
- 'title' => $title,
- 'route' => route(
- 'categories.show',
- [$category->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]
- ),
- 'total_transactions' => count($spent) + count($earned) + count($transferred),
- 'spent' => $this->groupByCurrency($spent),
- 'earned' => $this->groupByCurrency($earned),
- 'transferred' => $this->groupByCurrency($transferred),
- ];
- }
- $cache->store($entries);
-
- return $entries;
- }
-
- /**
- * Filter a list of journals by a set of dates, and then group them by currency.
- */
- private function filterJournalsByDate(array $array, Carbon $start, Carbon $end): array
- {
- $result = [];
-
- /** @var array $journal */
- foreach ($array as $journal) {
- if ($journal['date'] <= $end && $journal['date'] >= $start) {
- $result[] = $journal;
- }
- }
-
- return $result;
- }
-
- /**
- * Same as above, but for lists that involve transactions without a budget.
- *
- * This method has been refactored recently.
- *
- * @throws FireflyException
- */
- protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array
- {
- $range = Navigation::getViewRange(true);
-
- [$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
-
- $cache = new CacheProperties();
- $cache->addProperty($start);
- $cache->addProperty($end);
- $cache->addProperty($this->convertToPrimary);
- $cache->addProperty('no-budget-period-entries');
-
- if ($cache->has()) {
- return $cache->get();
- }
-
- /** @var array $dates */
- $dates = Navigation::blockPeriods($start, $end, $range);
- $entries = [];
-
- // get all expenses without a budget.
- /** @var GroupCollectorInterface $collector */
- $collector = app(GroupCollectorInterface::class);
- $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
- $journals = $collector->getExtractedJournals();
-
- foreach ($dates as $currentDate) {
- $set = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']);
- $title = Navigation::periodShow($currentDate['end'], $currentDate['period']);
- $entries[]
- = [
- 'title' => $title,
- 'route' => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
- 'total_transactions' => count($set),
- 'spent' => $this->groupByCurrency($set),
- 'earned' => [],
- 'transferred_away' => [],
- 'transferred_in' => [],
- ];
- }
- $cache->store($entries);
-
- return $entries;
- }
-
- /**
- * TODO fix the date.
- *
- * Show period overview for no category view.
- *
- * @throws FireflyException
- */
- protected function getNoCategoryPeriodOverview(Carbon $theDate): array
- {
- app('log')->debug(sprintf('Now in getNoCategoryPeriodOverview(%s)', $theDate->format('Y-m-d')));
- $range = Navigation::getViewRange(true);
- $first = $this->journalRepos->firstNull();
- $start = null === $first ? new Carbon() : $first->date;
- $end = clone $theDate;
- $end = Navigation::endOfPeriod($end, $range);
-
- app('log')->debug(sprintf('Start for getNoCategoryPeriodOverview() is %s', $start->format('Y-m-d')));
- app('log')->debug(sprintf('End for getNoCategoryPeriodOverview() is %s', $end->format('Y-m-d')));
-
- // properties for cache
- $dates = Navigation::blockPeriods($start, $end, $range);
- $entries = [];
-
- // collect all expenses in this period:
- /** @var GroupCollectorInterface $collector */
- $collector = app(GroupCollectorInterface::class);
- $collector->withoutCategory();
- $collector->setRange($start, $end);
- $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]);
- $earnedSet = $collector->getExtractedJournals();
-
- // collect all income in this period:
- /** @var GroupCollectorInterface $collector */
- $collector = app(GroupCollectorInterface::class);
- $collector->withoutCategory();
- $collector->setRange($start, $end);
- $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
- $spentSet = $collector->getExtractedJournals();
-
- // collect all transfers in this period:
- /** @var GroupCollectorInterface $collector */
- $collector = app(GroupCollectorInterface::class);
- $collector->withoutCategory();
- $collector->setRange($start, $end);
- $collector->setTypes([TransactionTypeEnum::TRANSFER->value]);
- $transferSet = $collector->getExtractedJournals();
-
- /** @var array $currentDate */
- foreach ($dates as $currentDate) {
- $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']);
- $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']);
- $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']);
- $title = Navigation::periodShow($currentDate['end'], $currentDate['period']);
- $entries[]
- = [
- 'title' => $title,
- 'route' => route('categories.no-category', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
- 'total_transactions' => count($spent) + count($earned) + count($transferred),
- 'spent' => $this->groupByCurrency($spent),
- 'earned' => $this->groupByCurrency($earned),
- 'transferred' => $this->groupByCurrency($transferred),
- ];
- }
- app('log')->debug('End of loops');
-
- return $entries;
- }
-
- /**
- * This shows a period overview for a tag. It goes back in time and lists all relevant transactions and sums.
- *
- * @throws FireflyException
- */
- protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array // period overview for tags.
- {
- $range = Navigation::getViewRange(true);
- [$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
-
- // properties for cache
- $cache = new CacheProperties();
- $cache->addProperty($start);
- $cache->addProperty($end);
- $cache->addProperty('tag-period-entries');
- $cache->addProperty($tag->id);
- if ($cache->has()) {
- return $cache->get();
- }
-
- /** @var array $dates */
- $dates = Navigation::blockPeriods($start, $end, $range);
- $entries = [];
-
- // collect all expenses in this period:
- /** @var GroupCollectorInterface $collector */
- $collector = app(GroupCollectorInterface::class);
- $collector->setTag($tag);
- $collector->setRange($start, $end);
- $collector->setTypes([TransactionTypeEnum::DEPOSIT->value]);
- $earnedSet = $collector->getExtractedJournals();
-
- // collect all income in this period:
- /** @var GroupCollectorInterface $collector */
- $collector = app(GroupCollectorInterface::class);
- $collector->setTag($tag);
- $collector->setRange($start, $end);
- $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value]);
- $spentSet = $collector->getExtractedJournals();
-
- // collect all transfers in this period:
- /** @var GroupCollectorInterface $collector */
- $collector = app(GroupCollectorInterface::class);
- $collector->setTag($tag);
- $collector->setRange($start, $end);
- $collector->setTypes([TransactionTypeEnum::TRANSFER->value]);
- $transferSet = $collector->getExtractedJournals();
-
- // filer all of them:
- $earnedSet = $this->filterJournalsByTag($earnedSet, $tag);
- $spentSet = $this->filterJournalsByTag($spentSet, $tag);
- $transferSet = $this->filterJournalsByTag($transferSet, $tag);
-
- foreach ($dates as $currentDate) {
- $spent = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']);
- $earned = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']);
- $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']);
- $title = Navigation::periodShow($currentDate['end'], $currentDate['period']);
- $entries[]
- = [
- 'transactions' => 0,
- 'title' => $title,
- 'route' => route(
- 'tags.show',
- [$tag->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]
- ),
- 'total_transactions' => count($spent) + count($earned) + count($transferred),
- 'spent' => $this->groupByCurrency($spent),
- 'earned' => $this->groupByCurrency($earned),
- 'transferred' => $this->groupByCurrency($transferred),
- ];
- }
-
- return $entries;
- }
-
- private function filterJournalsByTag(array $set, Tag $tag): array
- {
- $return = [];
- foreach ($set as $entry) {
- $found = false;
-
- /** @var array $localTag */
- foreach ($entry['tags'] as $localTag) {
- if ($localTag['id'] === $tag->id) {
- $found = true;
- }
- }
- if (false === $found) {
- continue;
- }
- $return[] = $entry;
- }
-
- return $return;
- }
-
- /**
- * @throws FireflyException
- */
- protected function getTransactionPeriodOverview(string $transactionType, Carbon $start, Carbon $end): array
- {
- $range = Navigation::getViewRange(true);
- $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType));
- [$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
-
- // properties for cache
- $cache = new CacheProperties();
- $cache->addProperty($start);
- $cache->addProperty($end);
- $cache->addProperty('transactions-period-entries');
- $cache->addProperty($transactionType);
- if ($cache->has()) {
- return $cache->get();
- }
-
- /** @var array $dates */
- $dates = Navigation::blockPeriods($start, $end, $range);
- $entries = [];
- $spent = [];
- $earned = [];
- $transferred = [];
- // collect all journals in this period (regardless of type)
- $collector = app(GroupCollectorInterface::class);
- $collector->setTypes($types)->setRange($start, $end);
- $genericSet = $collector->getExtractedJournals();
- $loops = 0;
-
- foreach ($dates as $currentDate) {
- $title = Navigation::periodShow($currentDate['end'], $currentDate['period']);
-
- if ($loops < 10) {
- // set to correct array
- if ('expenses' === $transactionType || 'withdrawal' === $transactionType) {
- $spent = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']);
- }
- if ('revenue' === $transactionType || 'deposit' === $transactionType) {
- $earned = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']);
- }
- if ('transfer' === $transactionType || 'transfers' === $transactionType) {
- $transferred = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']);
- }
- }
- $entries[]
- = [
- 'title' => $title,
- 'route' => route('transactions.index', [$transactionType, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
- 'total_transactions' => count($spent) + count($earned) + count($transferred),
- 'spent' => $this->groupByCurrency($spent),
- 'earned' => $this->groupByCurrency($earned),
- 'transferred' => $this->groupByCurrency($transferred),
- ];
- ++$loops;
- }
-
- return $entries;
- }
-
- /**
- * Return only transactions where $account is the source.
- */
- private function filterTransferredAway(Account $account, array $journals): array
- {
- $return = [];
-
- /** @var array $journal */
- foreach ($journals as $journal) {
- if ($account->id === (int)$journal['source_account_id']) {
- $return[] = $journal;
- }
- }
-
- return $return;
- }
-
- /**
- * Return only transactions where $account is the source.
- */
- private function filterTransferredIn(Account $account, array $journals): array
- {
- $return = [];
-
- /** @var array $journal */
- foreach ($journals as $journal) {
- if ($account->id === (int)$journal['destination_account_id']) {
- $return[] = $journal;
- }
+ ++$return['count'];
}
return $return;
diff --git a/app/Support/Http/Controllers/RenderPartialViews.php b/app/Support/Http/Controllers/RenderPartialViews.php
index 4c5f8348c6..f7d5df7cb8 100644
--- a/app/Support/Http/Controllers/RenderPartialViews.php
+++ b/app/Support/Http/Controllers/RenderPartialViews.php
@@ -56,10 +56,10 @@ trait RenderPartialViews
/** @var BudgetRepositoryInterface $budgetRepository */
$budgetRepository = app(BudgetRepositoryInterface::class);
- $budget = $budgetRepository->find((int) $attributes['budgetId']);
+ $budget = $budgetRepository->find((int)$attributes['budgetId']);
$accountRepos = app(AccountRepositoryInterface::class);
- $account = $accountRepos->find((int) $attributes['accountId']);
+ $account = $accountRepos->find((int)$attributes['accountId']);
if (null === $budget || null === $account) {
throw new FireflyException('Could not render popup.report.balance-amount because budget or account is null.');
@@ -115,7 +115,7 @@ trait RenderPartialViews
/** @var PopupReportInterface $popupHelper */
$popupHelper = app(PopupReportInterface::class);
- $budget = $budgetRepository->find((int) $attributes['budgetId']);
+ $budget = $budgetRepository->find((int)$attributes['budgetId']);
if (null === $budget) {
// transactions without a budget.
$budget = new Budget();
@@ -146,7 +146,7 @@ trait RenderPartialViews
/** @var CategoryRepositoryInterface $categoryRepository */
$categoryRepository = app(CategoryRepositoryInterface::class);
- $category = $categoryRepository->find((int) $attributes['categoryId']);
+ $category = $categoryRepository->find((int)$attributes['categoryId']);
$journals = $popupHelper->byCategory($category, $attributes);
try {
@@ -239,7 +239,7 @@ trait RenderPartialViews
/** @var PopupReportInterface $popupHelper */
$popupHelper = app(PopupReportInterface::class);
- $account = $accountRepository->find((int) $attributes['accountId']);
+ $account = $accountRepository->find((int)$attributes['accountId']);
if (null === $account) {
return 'This is an unknown account. Apologies.';
@@ -310,7 +310,7 @@ trait RenderPartialViews
$triggers = [];
foreach ($operators as $key => $operator) {
if ('user_action' !== $key && false === $operator['alias']) {
- $triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key));
+ $triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key));
}
}
asort($triggers);
@@ -325,7 +325,7 @@ trait RenderPartialViews
$count = ($index + 1);
try {
- $rootOperator = OperatorQuerySearch::getRootOperator((string) $entry->trigger_type);
+ $rootOperator = OperatorQuerySearch::getRootOperator((string)$entry->trigger_type);
if (str_starts_with($rootOperator, '-')) {
$rootOperator = substr($rootOperator, 1);
}
@@ -335,7 +335,7 @@ trait RenderPartialViews
'oldTrigger' => $rootOperator,
'oldValue' => $entry->trigger_value,
'oldChecked' => $entry->stop_processing,
- 'oldProhibited' => str_starts_with((string) $entry->trigger_type, '-'),
+ 'oldProhibited' => str_starts_with((string)$entry->trigger_type, '-'),
'count' => $count,
'triggers' => $triggers,
]
@@ -366,7 +366,7 @@ trait RenderPartialViews
/** @var PopupReportInterface $popupHelper */
$popupHelper = app(PopupReportInterface::class);
- $account = $accountRepository->find((int) $attributes['accountId']);
+ $account = $accountRepository->find((int)$attributes['accountId']);
if (null === $account) {
return 'This is an unknown category. Apologies.';
diff --git a/app/Support/Http/Controllers/RequestInformation.php b/app/Support/Http/Controllers/RequestInformation.php
index 6c7731b68f..39a6df3aff 100644
--- a/app/Support/Http/Controllers/RequestInformation.php
+++ b/app/Support/Http/Controllers/RequestInformation.php
@@ -32,9 +32,9 @@ use FireflyIII\Support\Binder\AccountList;
use FireflyIII\User;
use Illuminate\Contracts\Validation\Validator as ValidatorContract;
use Illuminate\Routing\Route;
-use Illuminate\Support\Facades\Validator;
-use Illuminate\Support\Facades\Route as RouteFacade;
use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\Route as RouteFacade;
+use Illuminate\Support\Facades\Validator;
use function Safe\parse_url;
@@ -54,6 +54,22 @@ trait RequestInformation
return $parts['host'] ?? '';
}
+ final protected function getPageName(): string // get request info
+ {
+ return str_replace('.', '_', RouteFacade::currentRouteName());
+ }
+
+ /**
+ * Get the specific name of a page for intro.
+ */
+ final protected function getSpecificPageName(): string // get request info
+ {
+ /** @var null|string $param */
+ $param = RouteFacade::current()->parameter('objectType');
+
+ return null === $param ? '' : sprintf('_%s', $param);
+ }
+
/**
* Get a list of triggers.
*/
@@ -67,7 +83,7 @@ trait RequestInformation
'type' => $triggerInfo['type'] ?? '',
'value' => $triggerInfo['value'] ?? '',
'prohibited' => $triggerInfo['prohibited'] ?? false,
- 'stop_processing' => 1 === (int) ($triggerInfo['stop_processing'] ?? '0'),
+ 'stop_processing' => 1 === (int)($triggerInfo['stop_processing'] ?? '0'),
];
$current = RuleFormRequest::replaceAmountTrigger($current);
$triggers[] = $current;
@@ -103,22 +119,6 @@ trait RequestInformation
return $shownDemo;
}
- final protected function getPageName(): string // get request info
- {
- return str_replace('.', '_', RouteFacade::currentRouteName());
- }
-
- /**
- * Get the specific name of a page for intro.
- */
- final protected function getSpecificPageName(): string // get request info
- {
- /** @var null|string $param */
- $param = RouteFacade::current()->parameter('objectType');
-
- return null === $param ? '' : sprintf('_%s', $param);
- }
-
/**
* Check if date is outside session range.
*/
@@ -172,11 +172,11 @@ trait RequestInformation
final protected function validatePassword(User $user, string $current, string $new): bool // get request info
{
if (!Hash::check($current, $user->password)) {
- throw new ValidationException((string) trans('firefly.invalid_current_password'));
+ throw new ValidationException((string)trans('firefly.invalid_current_password'));
}
if ($current === $new) {
- throw new ValidationException((string) trans('firefly.should_change'));
+ throw new ValidationException((string)trans('firefly.should_change'));
}
return true;
diff --git a/app/Support/Http/Controllers/RuleManagement.php b/app/Support/Http/Controllers/RuleManagement.php
index 6b88c3524c..081d1c44d0 100644
--- a/app/Support/Http/Controllers/RuleManagement.php
+++ b/app/Support/Http/Controllers/RuleManagement.php
@@ -51,7 +51,7 @@ trait RuleManagement
[
'oldAction' => $oldAction['type'],
'oldValue' => $oldAction['value'] ?? '',
- 'oldChecked' => 1 === (int) ($oldAction['stop_processing'] ?? '0'),
+ 'oldChecked' => 1 === (int)($oldAction['stop_processing'] ?? '0'),
'count' => $index + 1,
]
)->render();
@@ -78,7 +78,7 @@ trait RuleManagement
$triggers = [];
foreach ($operators as $key => $operator) {
if ('user_action' !== $key && false === $operator['alias']) {
- $triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key));
+ $triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key));
}
}
asort($triggers);
@@ -94,8 +94,8 @@ trait RuleManagement
[
'oldTrigger' => OperatorQuerySearch::getRootOperator($oldTrigger['type']),
'oldValue' => $oldTrigger['value'] ?? '',
- 'oldChecked' => 1 === (int) ($oldTrigger['stop_processing'] ?? '0'),
- 'oldProhibited' => 1 === (int) ($oldTrigger['prohibited'] ?? '0'),
+ 'oldChecked' => 1 === (int)($oldTrigger['stop_processing'] ?? '0'),
+ 'oldProhibited' => 1 === (int)($oldTrigger['prohibited'] ?? '0'),
'count' => $index + 1,
'triggers' => $triggers,
]
@@ -124,7 +124,7 @@ trait RuleManagement
$triggers = [];
foreach ($operators as $key => $operator) {
if ('user_action' !== $key && false === $operator['alias']) {
- $triggers[$key] = (string) trans(sprintf('firefly.rule_trigger_%s_choice', $key));
+ $triggers[$key] = (string)trans(sprintf('firefly.rule_trigger_%s_choice', $key));
}
}
asort($triggers);
@@ -132,7 +132,7 @@ trait RuleManagement
$index = 0;
foreach ($submittedOperators as $operator) {
$rootOperator = OperatorQuerySearch::getRootOperator($operator['type']);
- $needsContext = (bool) config(sprintf('search.operators.%s.needs_context', $rootOperator));
+ $needsContext = (bool)config(sprintf('search.operators.%s.needs_context', $rootOperator));
try {
$renderedEntries[] = view(
@@ -164,8 +164,8 @@ trait RuleManagement
$repository = app(RuleGroupRepositoryInterface::class);
if (0 === $repository->count()) {
$data = [
- 'title' => (string) trans('firefly.default_rule_group_name'),
- 'description' => (string) trans('firefly.default_rule_group_description'),
+ 'title' => (string)trans('firefly.default_rule_group_name'),
+ 'description' => (string)trans('firefly.default_rule_group_description'),
'active' => true,
];
diff --git a/app/Support/Http/Controllers/UserNavigation.php b/app/Support/Http/Controllers/UserNavigation.php
index 8fac232ad0..bee31f429e 100644
--- a/app/Support/Http/Controllers/UserNavigation.php
+++ b/app/Support/Http/Controllers/UserNavigation.php
@@ -49,7 +49,7 @@ trait UserNavigation
final protected function getPreviousUrl(string $identifier): string
{
app('log')->debug(sprintf('Trying to retrieve URL stored under "%s"', $identifier));
- $url = (string) session($identifier);
+ $url = (string)session($identifier);
app('log')->debug(sprintf('The URL is %s', $url));
return app('steam')->getSafeUrl($url, route('index'));
diff --git a/app/Support/JsonApi/Enrichments/AccountEnrichment.php b/app/Support/JsonApi/Enrichments/AccountEnrichment.php
index 62dee47082..7aedeb6880 100644
--- a/app/Support/JsonApi/Enrichments/AccountEnrichment.php
+++ b/app/Support/JsonApi/Enrichments/AccountEnrichment.php
@@ -53,29 +53,29 @@ use Override;
*/
class AccountEnrichment implements EnrichmentInterface
{
- private array $ids = [];
- private array $accountTypeIds = [];
- private array $accountTypes = [];
- private Collection $collection;
- private array $currencies = [];
- private array $locations = [];
- private array $meta = [];
- private readonly TransactionCurrency $primaryCurrency;
- private array $notes = [];
- private array $openingBalances = [];
- private User $user;
- private UserGroup $userGroup;
- private array $lastActivities = [];
- private ?Carbon $date = null;
- private ?Carbon $start = null;
- private ?Carbon $end = null;
+ private array $accountTypeIds = [];
+ private array $accountTypes = [];
+ private array $balances = [];
+ private Collection $collection;
private readonly bool $convertToPrimary;
- private array $balances = [];
- private array $startBalances = [];
- private array $endBalances = [];
- private array $objectGroups = [];
- private array $mappedObjects = [];
- private array $sort = [];
+ private array $currencies = [];
+ private ?Carbon $date = null;
+ private ?Carbon $end = null;
+ private array $endBalances = [];
+ private array $ids = [];
+ private array $lastActivities = [];
+ private array $locations = [];
+ private array $mappedObjects = [];
+ private array $meta = [];
+ private array $notes = [];
+ private array $objectGroups = [];
+ private array $openingBalances = [];
+ private readonly TransactionCurrency $primaryCurrency;
+ private array $sort = [];
+ private ?Carbon $start = null;
+ private array $startBalances = [];
+ private User $user;
+ private UserGroup $userGroup;
/**
* TODO The account enricher must do conversion from and to the primary currency.
@@ -86,16 +86,6 @@ class AccountEnrichment implements EnrichmentInterface
$this->convertToPrimary = Amount::convertToPrimary();
}
- #[Override]
- public function enrichSingle(array|Model $model): Account|array
- {
- Log::debug(__METHOD__);
- $collection = new Collection()->push($model);
- $collection = $this->enrich($collection);
-
- return $collection->first();
- }
-
#[Override]
/**
* Do the actual enrichment.
@@ -121,114 +111,47 @@ class AccountEnrichment implements EnrichmentInterface
return $this->collection;
}
- private function collectIds(): void
+ #[Override]
+ public function enrichSingle(array|Model $model): Account|array
{
- /** @var Account $account */
- foreach ($this->collection as $account) {
- $this->ids[] = (int)$account->id;
- $this->accountTypeIds[] = (int)$account->account_type_id;
- }
- $this->ids = array_unique($this->ids);
- $this->accountTypeIds = array_unique($this->accountTypeIds);
+ Log::debug(__METHOD__);
+ $collection = new Collection()->push($model);
+ $collection = $this->enrich($collection);
+
+ return $collection->first();
}
- private function getAccountTypes(): void
+ public function getDate(): Carbon
{
- $types = AccountType::whereIn('id', $this->accountTypeIds)->get();
-
- /** @var AccountType $type */
- foreach ($types as $type) {
- $this->accountTypes[(int)$type->id] = $type->type;
+ if (!$this->date instanceof Carbon) {
+ return now();
}
+
+ return $this->date;
}
- private function collectMetaData(): void
+ public function setDate(?Carbon $date): void
{
- $set = AccountMeta::whereIn('name', ['is_multi_currency', 'include_net_worth', 'currency_id', 'account_role', 'account_number', 'BIC', 'liability_direction', 'interest', 'interest_period', 'current_debt'])
- ->whereIn('account_id', $this->ids)
- ->get(['account_meta.id', 'account_meta.account_id', 'account_meta.name', 'account_meta.data'])->toArray()
- ;
-
- /** @var array $entry */
- foreach ($set as $entry) {
- $this->meta[(int)$entry['account_id']][$entry['name']] = (string)$entry['data'];
- if ('currency_id' === $entry['name']) {
- $this->currencies[(int)$entry['data']] = true;
- }
- }
- if (count($this->currencies) > 0) {
- $currencies = TransactionCurrency::whereIn('id', array_keys($this->currencies))->get();
- foreach ($currencies as $currency) {
- $this->currencies[(int)$currency->id] = $currency;
- }
- }
- $this->currencies[0] = $this->primaryCurrency;
- foreach ($this->currencies as $id => $currency) {
- if (true === $currency) {
- throw new FireflyException(sprintf('Currency #%d not found.', $id));
- }
+ if ($date instanceof Carbon) {
+ $date->endOfDay();
+ Log::debug(sprintf('Date is now %s', $date->toW3cString()));
}
+ $this->date = $date;
}
- private function collectNotes(): void
+ public function setEnd(?Carbon $end): void
{
- $notes = Note::query()->whereIn('noteable_id', $this->ids)
- ->whereNotNull('notes.text')
- ->where('notes.text', '!=', '')
- ->where('noteable_type', Account::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
- ;
- foreach ($notes as $note) {
- $this->notes[(int)$note['noteable_id']] = (string)$note['text'];
- }
- Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
+ $this->end = $end;
}
- private function collectLocations(): void
+ public function setSort(array $sort): void
{
- $locations = Location::query()->whereIn('locatable_id', $this->ids)
- ->where('locatable_type', Account::class)->get(['locations.locatable_id', 'locations.latitude', 'locations.longitude', 'locations.zoom_level'])->toArray()
- ;
- foreach ($locations as $location) {
- $this->locations[(int)$location['locatable_id']]
- = [
- 'latitude' => (float)$location['latitude'],
- 'longitude' => (float)$location['longitude'],
- 'zoom_level' => (int)$location['zoom_level'],
- ];
- }
- Log::debug(sprintf('Enrich with %d locations(s)', count($this->locations)));
+ $this->sort = $sort;
}
- private function collectOpeningBalances(): void
+ public function setStart(?Carbon $start): void
{
- // use new group collector:
- /** @var GroupCollectorInterface $collector */
- $collector = app(GroupCollectorInterface::class);
- $collector
- ->setUser($this->user)
- ->setUserGroup($this->userGroup)
- ->setAccounts($this->collection)
- ->withAccountInformation()
- ->setTypes([TransactionTypeEnum::OPENING_BALANCE->value])
- ;
- $journals = $collector->getExtractedJournals();
- foreach ($journals as $journal) {
- $this->openingBalances[(int)$journal['source_account_id']]
- = [
- 'amount' => Steam::negative($journal['amount']),
- 'date' => $journal['date'],
- ];
- $this->openingBalances[(int)$journal['destination_account_id']]
- = [
- 'amount' => Steam::positive($journal['amount']),
- 'date' => $journal['date'],
- ];
- }
- }
-
- public function setUserGroup(UserGroup $userGroup): void
- {
- $this->userGroup = $userGroup;
+ $this->start = $start;
}
public function setUser(User $user): void
@@ -237,6 +160,11 @@ class AccountEnrichment implements EnrichmentInterface
$this->userGroup = $user->userGroup;
}
+ public function setUserGroup(UserGroup $userGroup): void
+ {
+ $this->userGroup = $userGroup;
+ }
+
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (Account $item) {
@@ -357,11 +285,6 @@ class AccountEnrichment implements EnrichmentInterface
});
}
- private function collectLastActivities(): void
- {
- $this->lastActivities = Steam::getLastActivities($this->ids);
- }
-
private function collectBalances(): void
{
$this->balances = Steam::accountsBalancesOptimized($this->collection, $this->getDate(), $this->primaryCurrency, $this->convertToPrimary);
@@ -371,6 +294,79 @@ class AccountEnrichment implements EnrichmentInterface
}
}
+ private function collectIds(): void
+ {
+ /** @var Account $account */
+ foreach ($this->collection as $account) {
+ $this->ids[] = (int)$account->id;
+ $this->accountTypeIds[] = (int)$account->account_type_id;
+ }
+ $this->ids = array_unique($this->ids);
+ $this->accountTypeIds = array_unique($this->accountTypeIds);
+ }
+
+ private function collectLastActivities(): void
+ {
+ $this->lastActivities = Steam::getLastActivities($this->ids);
+ }
+
+ private function collectLocations(): void
+ {
+ $locations = Location::query()->whereIn('locatable_id', $this->ids)
+ ->where('locatable_type', Account::class)->get(['locations.locatable_id', 'locations.latitude', 'locations.longitude', 'locations.zoom_level'])->toArray()
+ ;
+ foreach ($locations as $location) {
+ $this->locations[(int)$location['locatable_id']]
+ = [
+ 'latitude' => (float)$location['latitude'],
+ 'longitude' => (float)$location['longitude'],
+ 'zoom_level' => (int)$location['zoom_level'],
+ ];
+ }
+ Log::debug(sprintf('Enrich with %d locations(s)', count($this->locations)));
+ }
+
+ private function collectMetaData(): void
+ {
+ $set = AccountMeta::whereIn('name', ['is_multi_currency', 'include_net_worth', 'currency_id', 'account_role', 'account_number', 'BIC', 'liability_direction', 'interest', 'interest_period', 'current_debt'])
+ ->whereIn('account_id', $this->ids)
+ ->get(['account_meta.id', 'account_meta.account_id', 'account_meta.name', 'account_meta.data'])->toArray()
+ ;
+
+ /** @var array $entry */
+ foreach ($set as $entry) {
+ $this->meta[(int)$entry['account_id']][$entry['name']] = (string)$entry['data'];
+ if ('currency_id' === $entry['name']) {
+ $this->currencies[(int)$entry['data']] = true;
+ }
+ }
+ if (count($this->currencies) > 0) {
+ $currencies = TransactionCurrency::whereIn('id', array_keys($this->currencies))->get();
+ foreach ($currencies as $currency) {
+ $this->currencies[(int)$currency->id] = $currency;
+ }
+ }
+ $this->currencies[0] = $this->primaryCurrency;
+ foreach ($this->currencies as $id => $currency) {
+ if (true === $currency) {
+ throw new FireflyException(sprintf('Currency #%d not found.', $id));
+ }
+ }
+ }
+
+ private function collectNotes(): void
+ {
+ $notes = Note::query()->whereIn('noteable_id', $this->ids)
+ ->whereNotNull('notes.text')
+ ->where('notes.text', '!=', '')
+ ->where('noteable_type', Account::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
+ ;
+ foreach ($notes as $note) {
+ $this->notes[(int)$note['noteable_id']] = (string)$note['text'];
+ }
+ Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
+ }
+
private function collectObjectGroups(): void
{
$set = DB::table('object_groupables')
@@ -393,32 +389,41 @@ class AccountEnrichment implements EnrichmentInterface
}
}
- public function setDate(?Carbon $date): void
+ private function collectOpeningBalances(): void
{
- if ($date instanceof Carbon) {
- $date->endOfDay();
- Log::debug(sprintf('Date is now %s', $date->toW3cString()));
+ // use new group collector:
+ /** @var GroupCollectorInterface $collector */
+ $collector = app(GroupCollectorInterface::class);
+ $collector
+ ->setUser($this->user)
+ ->setUserGroup($this->userGroup)
+ ->setAccounts($this->collection)
+ ->withAccountInformation()
+ ->setTypes([TransactionTypeEnum::OPENING_BALANCE->value])
+ ;
+ $journals = $collector->getExtractedJournals();
+ foreach ($journals as $journal) {
+ $this->openingBalances[(int)$journal['source_account_id']]
+ = [
+ 'amount' => Steam::negative($journal['amount']),
+ 'date' => $journal['date'],
+ ];
+ $this->openingBalances[(int)$journal['destination_account_id']]
+ = [
+ 'amount' => Steam::positive($journal['amount']),
+ 'date' => $journal['date'],
+ ];
}
- $this->date = $date;
}
- public function getDate(): Carbon
+ private function getAccountTypes(): void
{
- if (!$this->date instanceof Carbon) {
- return now();
+ $types = AccountType::whereIn('id', $this->accountTypeIds)->get();
+
+ /** @var AccountType $type */
+ foreach ($types as $type) {
+ $this->accountTypes[(int)$type->id] = $type->type;
}
-
- return $this->date;
- }
-
- public function setStart(?Carbon $start): void
- {
- $this->start = $start;
- }
-
- public function setEnd(?Carbon $end): void
- {
- $this->end = $end;
}
private function getBalanceDifference(int $id, TransactionCurrency $currency): ?string
@@ -437,11 +442,6 @@ class AccountEnrichment implements EnrichmentInterface
return bcsub($end, $start);
}
- public function setSort(array $sort): void
- {
- $this->sort = $sort;
- }
-
private function sortData(): void
{
$dbParams = config('firefly.allowed_db_sort_parameters.Account', []);
diff --git a/app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php b/app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php
index 6154c941aa..a6a8823368 100644
--- a/app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php
+++ b/app/Support/JsonApi/Enrichments/AvailableBudgetEnrichment.php
@@ -40,20 +40,20 @@ use Override;
class AvailableBudgetEnrichment implements EnrichmentInterface
{
- private User $user; // @phpstan-ignore-line
- private UserGroup $userGroup; // @phpstan-ignore-line
- private readonly bool $convertToPrimary;
- private array $ids = [];
- private array $currencyIds = [];
+ private Collection $collection; // @phpstan-ignore-line
+ private readonly bool $convertToPrimary; // @phpstan-ignore-line
private array $currencies = [];
- private Collection $collection;
- private array $spentInBudgets = [];
- private array $spentOutsideBudgets = [];
- private array $pcSpentInBudgets = [];
- private array $pcSpentOutsideBudgets = [];
+ private array $currencyIds = [];
+ private array $ids = [];
private readonly NoBudgetRepositoryInterface $noBudgetRepository;
private readonly OperationsRepositoryInterface $opsRepository;
+ private array $pcSpentInBudgets = [];
+ private array $pcSpentOutsideBudgets = [];
private readonly BudgetRepositoryInterface $repository;
+ private array $spentInBudgets = [];
+ private array $spentOutsideBudgets = [];
+ private User $user;
+ private UserGroup $userGroup;
public function __construct()
{
@@ -104,6 +104,34 @@ class AvailableBudgetEnrichment implements EnrichmentInterface
$this->repository->setUserGroup($userGroup);
}
+ private function appendCollectedData(): void
+ {
+ $this->collection = $this->collection->map(function (AvailableBudget $item) {
+ $id = (int)$item->id;
+ $currencyId = $this->currencyIds[$id];
+ $currency = $this->currencies[$currencyId];
+ $meta = [
+ 'currency' => $currency,
+ 'spent_in_budgets' => $this->spentInBudgets[$id] ?? [],
+ 'pc_spent_in_budgets' => $this->pcSpentInBudgets[$id] ?? [],
+ 'spent_outside_budgets' => $this->spentOutsideBudgets[$id] ?? [],
+ 'pc_spent_outside_budgets' => $this->pcSpentOutsideBudgets[$id] ?? [],
+ ];
+ $item->meta = $meta;
+
+ return $item;
+ });
+ }
+
+ private function collectCurrencies(): void
+ {
+ $ids = array_unique(array_values($this->currencyIds));
+ $set = TransactionCurrency::whereIn('id', $ids)->get();
+ foreach ($set as $currency) {
+ $this->currencies[(int)$currency->id] = $currency;
+ }
+ }
+
private function collectIds(): void
{
/** @var AvailableBudget $availableBudget */
@@ -138,32 +166,4 @@ class AvailableBudgetEnrichment implements EnrichmentInterface
}
}
}
-
- private function appendCollectedData(): void
- {
- $this->collection = $this->collection->map(function (AvailableBudget $item) {
- $id = (int)$item->id;
- $currencyId = $this->currencyIds[$id];
- $currency = $this->currencies[$currencyId];
- $meta = [
- 'currency' => $currency,
- 'spent_in_budgets' => $this->spentInBudgets[$id] ?? [],
- 'pc_spent_in_budgets' => $this->pcSpentInBudgets[$id] ?? [],
- 'spent_outside_budgets' => $this->spentOutsideBudgets[$id] ?? [],
- 'pc_spent_outside_budgets' => $this->pcSpentOutsideBudgets[$id] ?? [],
- ];
- $item->meta = $meta;
-
- return $item;
- });
- }
-
- private function collectCurrencies(): void
- {
- $ids = array_unique(array_values($this->currencyIds));
- $set = TransactionCurrency::whereIn('id', $ids)->get();
- foreach ($set as $currency) {
- $this->currencies[(int)$currency->id] = $currency;
- }
- }
}
diff --git a/app/Support/JsonApi/Enrichments/BudgetEnrichment.php b/app/Support/JsonApi/Enrichments/BudgetEnrichment.php
index 2aa21bc9cf..cb875aca78 100644
--- a/app/Support/JsonApi/Enrichments/BudgetEnrichment.php
+++ b/app/Support/JsonApi/Enrichments/BudgetEnrichment.php
@@ -40,19 +40,19 @@ use Illuminate\Support\Facades\Log;
class BudgetEnrichment implements EnrichmentInterface
{
- private Collection $collection;
- private User $user;
- private UserGroup $userGroup;
- private array $ids = [];
- private array $notes = [];
- private array $autoBudgets = [];
- private array $currencies = [];
- private ?Carbon $start = null;
- private ?Carbon $end = null;
- private array $spent = [];
- private array $pcSpent = [];
- private array $objectGroups = [];
- private array $mappedObjects = [];
+ private array $autoBudgets = [];
+ private Collection $collection;
+ private array $currencies = [];
+ private ?Carbon $end = null;
+ private array $ids = [];
+ private array $mappedObjects = [];
+ private array $notes = [];
+ private array $objectGroups = [];
+ private array $pcSpent = [];
+ private array $spent = [];
+ private ?Carbon $start = null;
+ private User $user;
+ private UserGroup $userGroup;
public function __construct() {}
@@ -79,6 +79,16 @@ class BudgetEnrichment implements EnrichmentInterface
return $collection->first();
}
+ public function setEnd(?Carbon $end): void
+ {
+ $this->end = $end;
+ }
+
+ public function setStart(?Carbon $start): void
+ {
+ $this->start = $start;
+ }
+
public function setUser(User $user): void
{
$this->user = $user;
@@ -90,28 +100,6 @@ class BudgetEnrichment implements EnrichmentInterface
$this->userGroup = $userGroup;
}
- private function collectIds(): void
- {
- /** @var Budget $budget */
- foreach ($this->collection as $budget) {
- $this->ids[] = (int)$budget->id;
- }
- $this->ids = array_unique($this->ids);
- }
-
- private function collectNotes(): void
- {
- $notes = Note::query()->whereIn('noteable_id', $this->ids)
- ->whereNotNull('notes.text')
- ->where('notes.text', '!=', '')
- ->where('noteable_type', Budget::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
- ;
- foreach ($notes as $note) {
- $this->notes[(int)$note['noteable_id']] = (string)$note['text'];
- }
- Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
- }
-
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (Budget $item) {
@@ -130,7 +118,7 @@ class BudgetEnrichment implements EnrichmentInterface
// add object group if available
if (array_key_exists($id, $this->mappedObjects)) {
$key = $this->mappedObjects[$id];
- $meta['object_group_id'] = (string) $this->objectGroups[$key]['id'];
+ $meta['object_group_id'] = (string)$this->objectGroups[$key]['id'];
$meta['object_group_title'] = $this->objectGroups[$key]['title'];
$meta['object_group_order'] = $this->objectGroups[$key]['order'];
}
@@ -177,14 +165,26 @@ class BudgetEnrichment implements EnrichmentInterface
}
}
- public function setEnd(?Carbon $end): void
+ private function collectIds(): void
{
- $this->end = $end;
+ /** @var Budget $budget */
+ foreach ($this->collection as $budget) {
+ $this->ids[] = (int)$budget->id;
+ }
+ $this->ids = array_unique($this->ids);
}
- public function setStart(?Carbon $start): void
+ private function collectNotes(): void
{
- $this->start = $start;
+ $notes = Note::query()->whereIn('noteable_id', $this->ids)
+ ->whereNotNull('notes.text')
+ ->where('notes.text', '!=', '')
+ ->where('noteable_type', Budget::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
+ ;
+ foreach ($notes as $note) {
+ $this->notes[(int)$note['noteable_id']] = (string)$note['text'];
+ }
+ Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
}
private function collectObjectGroups(): void
diff --git a/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php b/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php
index 87e8b7a0a8..db1e248708 100644
--- a/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php
+++ b/app/Support/JsonApi/Enrichments/BudgetLimitEnrichment.php
@@ -40,19 +40,19 @@ use Illuminate\Support\Facades\Log;
class BudgetLimitEnrichment implements EnrichmentInterface
{
- private User $user;
- private UserGroup $userGroup; // @phpstan-ignore-line
- private Collection $collection;
- private array $ids = [];
- private array $notes = [];
- private Carbon $start;
- private Carbon $end;
- private array $expenses = [];
- private array $pcExpenses = [];
- private array $currencyIds = [];
- private array $currencies = [];
- private bool $convertToPrimary = true;
+ private Collection $collection;
+ private bool $convertToPrimary = true; // @phpstan-ignore-line
+ private array $currencies = [];
+ private array $currencyIds = [];
+ private Carbon $end;
+ private array $expenses = [];
+ private array $ids = [];
+ private array $notes = [];
+ private array $pcExpenses = [];
private readonly TransactionCurrency $primaryCurrency;
+ private Carbon $start;
+ private User $user;
+ private UserGroup $userGroup;
public function __construct()
{
@@ -93,36 +93,6 @@ class BudgetLimitEnrichment implements EnrichmentInterface
$this->userGroup = $userGroup;
}
- private function collectIds(): void
- {
- $this->start = $this->collection->min('start_date') ?? Carbon::now()->startOfMonth();
- $this->end = $this->collection->max('end_date') ?? Carbon::now()->endOfMonth();
-
- /** @var BudgetLimit $limit */
- foreach ($this->collection as $limit) {
- $id = (int)$limit->id;
- $this->ids[] = $id;
- if (0 !== (int)$limit->transaction_currency_id) {
- $this->currencyIds[$id] = (int)$limit->transaction_currency_id;
- }
- }
- $this->ids = array_unique($this->ids);
- $this->currencyIds = array_unique($this->currencyIds);
- }
-
- private function collectNotes(): void
- {
- $notes = Note::query()->whereIn('noteable_id', $this->ids)
- ->whereNotNull('notes.text')
- ->where('notes.text', '!=', '')
- ->where('noteable_type', BudgetLimit::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
- ;
- foreach ($notes as $note) {
- $this->notes[(int)$note['noteable_id']] = (string)$note['text'];
- }
- Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
- }
-
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (BudgetLimit $item) {
@@ -154,9 +124,10 @@ class BudgetLimitEnrichment implements EnrichmentInterface
/** @var BudgetLimit $budgetLimit */
foreach ($this->collection as $budgetLimit) {
+ Log::debug(sprintf('Filtering expenses for budget limit #%d (budget #%d)', $budgetLimit->id, $budgetLimit->budget_id));
$id = (int)$budgetLimit->id;
$filteredExpenses = $this->filterToBudget($expenses, $budgetLimit->budget_id);
- $filteredExpenses = $repository->sumCollectedExpenses($expenses, $budgetLimit->start_date, $budgetLimit->end_date, $budgetLimit->transactionCurrency, false);
+ $filteredExpenses = $repository->sumCollectedExpenses($filteredExpenses, $budgetLimit->start_date, $budgetLimit->end_date, $budgetLimit->transactionCurrency, false);
$this->expenses[$id] = array_values($filteredExpenses);
if (true === $this->convertToPrimary && $budgetLimit->transactionCurrency->id !== $this->primaryCurrency->id) {
@@ -178,6 +149,44 @@ class BudgetLimitEnrichment implements EnrichmentInterface
}
}
+ private function collectIds(): void
+ {
+ $this->start = $this->collection->min('start_date') ?? Carbon::now()->startOfMonth();
+ $this->end = $this->collection->max('end_date') ?? Carbon::now()->endOfMonth();
+
+ /** @var BudgetLimit $limit */
+ foreach ($this->collection as $limit) {
+ $id = (int)$limit->id;
+ $this->ids[] = $id;
+ if (0 !== (int)$limit->transaction_currency_id) {
+ $this->currencyIds[$id] = (int)$limit->transaction_currency_id;
+ }
+ }
+ $this->ids = array_unique($this->ids);
+ $this->currencyIds = array_unique($this->currencyIds);
+ }
+
+ private function collectNotes(): void
+ {
+ $notes = Note::query()->whereIn('noteable_id', $this->ids)
+ ->whereNotNull('notes.text')
+ ->where('notes.text', '!=', '')
+ ->where('noteable_type', BudgetLimit::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
+ ;
+ foreach ($notes as $note) {
+ $this->notes[(int)$note['noteable_id']] = (string)$note['text'];
+ }
+ Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
+ }
+
+ private function filterToBudget(array $expenses, int $budget): array
+ {
+ $result = array_filter($expenses, fn (array $item) => (int)$item['budget_id'] === $budget);
+ Log::debug(sprintf('filterToBudget for budget #%d, from %d to %d items', $budget, count($expenses), count($result)));
+
+ return $result;
+ }
+
private function stringifyIds(): void
{
$this->expenses = array_map(fn ($first) => array_map(function ($second) {
@@ -192,9 +201,4 @@ class BudgetLimitEnrichment implements EnrichmentInterface
return $second;
}, $first), $this->expenses);
}
-
- private function filterToBudget(array $expenses, int $budget): array
- {
- return array_filter($expenses, fn (array $item) => (int)$item['budget_id'] === $budget);
- }
}
diff --git a/app/Support/JsonApi/Enrichments/CategoryEnrichment.php b/app/Support/JsonApi/Enrichments/CategoryEnrichment.php
index bbe24f94c2..b5ddd22c52 100644
--- a/app/Support/JsonApi/Enrichments/CategoryEnrichment.php
+++ b/app/Support/JsonApi/Enrichments/CategoryEnrichment.php
@@ -38,18 +38,18 @@ use Illuminate\Support\Facades\Log;
class CategoryEnrichment implements EnrichmentInterface
{
private Collection $collection;
- private User $user;
- private UserGroup $userGroup;
+ private array $earned = [];
+ private ?Carbon $end = null;
private array $ids = [];
private array $notes = [];
- private ?Carbon $start = null;
- private ?Carbon $end = null;
- private array $spent = [];
- private array $pcSpent = [];
- private array $earned = [];
private array $pcEarned = [];
- private array $transfers = [];
+ private array $pcSpent = [];
private array $pcTransfers = [];
+ private array $spent = [];
+ private ?Carbon $start = null;
+ private array $transfers = [];
+ private User $user;
+ private UserGroup $userGroup;
public function enrich(Collection $collection): Collection
{
@@ -71,6 +71,16 @@ class CategoryEnrichment implements EnrichmentInterface
return $collection->first();
}
+ public function setEnd(?Carbon $end): void
+ {
+ $this->end = $end;
+ }
+
+ public function setStart(?Carbon $start): void
+ {
+ $this->start = $start;
+ }
+
public function setUser(User $user): void
{
$this->user = $user;
@@ -82,15 +92,6 @@ class CategoryEnrichment implements EnrichmentInterface
$this->userGroup = $userGroup;
}
- private function collectIds(): void
- {
- /** @var Category $category */
- foreach ($this->collection as $category) {
- $this->ids[] = (int)$category->id;
- }
- $this->ids = array_unique($this->ids);
- }
-
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (Category $item) {
@@ -110,14 +111,13 @@ class CategoryEnrichment implements EnrichmentInterface
});
}
- public function setEnd(?Carbon $end): void
+ private function collectIds(): void
{
- $this->end = $end;
- }
-
- public function setStart(?Carbon $start): void
- {
- $this->start = $start;
+ /** @var Category $category */
+ foreach ($this->collection as $category) {
+ $this->ids[] = (int)$category->id;
+ }
+ $this->ids = array_unique($this->ids);
}
private function collectNotes(): void
@@ -135,7 +135,7 @@ class CategoryEnrichment implements EnrichmentInterface
private function collectTransactions(): void
{
- if (null !== $this->start && null !== $this->end) {
+ if ($this->start instanceof Carbon && $this->end instanceof Carbon) {
/** @var OperationsRepositoryInterface $opsRepository */
$opsRepository = app(OperationsRepositoryInterface::class);
$opsRepository->setUser($this->user);
diff --git a/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php b/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php
index 9dd940801a..7c6a8a8a5b 100644
--- a/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php
+++ b/app/Support/JsonApi/Enrichments/PiggyBankEnrichment.php
@@ -43,20 +43,20 @@ use Illuminate\Support\Facades\Log;
class PiggyBankEnrichment implements EnrichmentInterface
{
- private User $user; // @phpstan-ignore-line
- private UserGroup $userGroup; // @phpstan-ignore-line
- private Collection $collection;
- private array $ids = [];
- private array $currencyIds = [];
- private array $currencies = [];
- private array $accountIds = [];
+ private array $accountIds = []; // @phpstan-ignore-line
+ private array $accounts = []; // @phpstan-ignore-line
+ private array $amounts = [];
+ private Collection $collection;
+ private array $currencies = [];
+ private array $currencyIds = [];
+ private array $ids = [];
// private array $accountCurrencies = [];
- private array $notes = [];
- private array $mappedObjects = [];
+ private array $mappedObjects = [];
+ private array $notes = [];
+ private array $objectGroups = [];
private readonly TransactionCurrency $primaryCurrency;
- private array $amounts = [];
- private array $accounts = [];
- private array $objectGroups = [];
+ private User $user;
+ private UserGroup $userGroup;
public function __construct()
{
@@ -97,69 +97,6 @@ class PiggyBankEnrichment implements EnrichmentInterface
$this->userGroup = $userGroup;
}
- private function collectIds(): void
- {
- /** @var PiggyBank $piggy */
- foreach ($this->collection as $piggy) {
- $id = (int)$piggy->id;
- $this->ids[] = $id;
- $this->currencyIds[$id] = (int)$piggy->transaction_currency_id;
- }
- $this->ids = array_unique($this->ids);
-
- // collect currencies.
- $currencies = TransactionCurrency::whereIn('id', $this->currencyIds)->get();
- foreach ($currencies as $currency) {
- $this->currencies[(int)$currency->id] = $currency;
- }
-
- // collect accounts
- $set = DB::table('account_piggy_bank')->whereIn('piggy_bank_id', $this->ids)->get(['piggy_bank_id', 'account_id', 'current_amount', 'native_current_amount']);
- foreach ($set as $item) {
- $id = (int)$item->piggy_bank_id;
- $accountId = (int)$item->account_id;
- $this->amounts[$id] ??= [];
- if (!array_key_exists($id, $this->accountIds)) {
- $this->accountIds[$id] = (int)$item->account_id;
- }
- if (!array_key_exists($accountId, $this->amounts[$id])) {
- $this->amounts[$id][$accountId] = [
- 'current_amount' => '0',
- 'pc_current_amount' => '0',
- ];
- }
- $this->amounts[$id][$accountId]['current_amount'] = bcadd($this->amounts[$id][$accountId]['current_amount'], (string) $item->current_amount);
- if (null !== $this->amounts[$id][$accountId]['pc_current_amount'] && null !== $item->native_current_amount) {
- $this->amounts[$id][$accountId]['pc_current_amount'] = bcadd($this->amounts[$id][$accountId]['pc_current_amount'], $item->native_current_amount);
- }
- }
-
- // get account currency preference for ALL.
- $set = AccountMeta::whereIn('account_id', array_values($this->accountIds))->where('name', 'currency_id')->get();
-
- /** @var AccountMeta $item */
- foreach ($set as $item) {
- $accountId = (int)$item->account_id;
- $currencyId = (int)$item->data;
- if (!array_key_exists($currencyId, $this->currencies)) {
- $this->currencies[$currencyId] = Amount::getTransactionCurrencyById($currencyId);
- }
- // $this->accountCurrencies[$accountId] = $this->currencies[$currencyId];
- }
-
- // get account info.
- $set = Account::whereIn('id', array_values($this->accountIds))->get();
-
- /** @var Account $item */
- foreach ($set as $item) {
- $id = (int)$item->id;
- $this->accounts[$id] = [
- 'id' => $id,
- 'name' => $item->name,
- ];
- }
- }
-
private function appendCollectedData(): void
{
$this->collection = $this->collection->map(function (PiggyBank $item) {
@@ -193,7 +130,7 @@ class PiggyBankEnrichment implements EnrichmentInterface
// add object group if available
if (array_key_exists($id, $this->mappedObjects)) {
$key = $this->mappedObjects[$id];
- $meta['object_group_id'] = (string) $this->objectGroups[$key]['id'];
+ $meta['object_group_id'] = (string)$this->objectGroups[$key]['id'];
$meta['object_group_title'] = $this->objectGroups[$key]['title'];
$meta['object_group_order'] = $this->objectGroups[$key]['order'];
}
@@ -205,9 +142,9 @@ class PiggyBankEnrichment implements EnrichmentInterface
'current_amount' => Steam::bcround($row['current_amount'], $currency->decimal_places),
'pc_current_amount' => Steam::bcround($row['pc_current_amount'], $this->primaryCurrency->decimal_places),
];
- $meta['current_amount'] = bcadd($meta['current_amount'], $row['current_amount']);
+ $meta['current_amount'] = bcadd($meta['current_amount'], (string) $row['current_amount']);
// only add pc_current_amount when the pc_current_amount is set
- $meta['pc_current_amount'] = null === $row['pc_current_amount'] ? null : bcadd($meta['pc_current_amount'], $row['pc_current_amount']);
+ $meta['pc_current_amount'] = null === $row['pc_current_amount'] ? null : bcadd((string) $meta['pc_current_amount'], (string) $row['pc_current_amount']);
}
$meta['current_amount'] = Steam::bcround($meta['current_amount'], $currency->decimal_places);
// only round this number when pc_current_amount is set.
@@ -215,8 +152,8 @@ class PiggyBankEnrichment implements EnrichmentInterface
// calculate left to save, only when there is a target amount.
if (null !== $targetAmount) {
- $meta['left_to_save'] = bcsub($meta['target_amount'], $meta['current_amount']);
- $meta['pc_left_to_save'] = null === $meta['pc_target_amount'] ? null : bcsub($meta['pc_target_amount'], $meta['pc_current_amount']);
+ $meta['left_to_save'] = bcsub((string) $meta['target_amount'], (string) $meta['current_amount']);
+ $meta['pc_left_to_save'] = null === $meta['pc_target_amount'] ? null : bcsub((string) $meta['pc_target_amount'], (string) $meta['pc_current_amount']);
}
// get suggested per month.
@@ -229,6 +166,71 @@ class PiggyBankEnrichment implements EnrichmentInterface
});
}
+ private function collectCurrentAmounts(): void {}
+
+ private function collectIds(): void
+ {
+ /** @var PiggyBank $piggy */
+ foreach ($this->collection as $piggy) {
+ $id = (int)$piggy->id;
+ $this->ids[] = $id;
+ $this->currencyIds[$id] = (int)$piggy->transaction_currency_id;
+ }
+ $this->ids = array_unique($this->ids);
+
+ // collect currencies.
+ $currencies = TransactionCurrency::whereIn('id', $this->currencyIds)->get();
+ foreach ($currencies as $currency) {
+ $this->currencies[(int)$currency->id] = $currency;
+ }
+
+ // collect accounts
+ $set = DB::table('account_piggy_bank')->whereIn('piggy_bank_id', $this->ids)->get(['piggy_bank_id', 'account_id', 'current_amount', 'native_current_amount']);
+ foreach ($set as $item) {
+ $id = (int)$item->piggy_bank_id;
+ $accountId = (int)$item->account_id;
+ $this->amounts[$id] ??= [];
+ if (!array_key_exists($id, $this->accountIds)) {
+ $this->accountIds[$id] = (int)$item->account_id;
+ }
+ if (!array_key_exists($accountId, $this->amounts[$id])) {
+ $this->amounts[$id][$accountId] = [
+ 'current_amount' => '0',
+ 'pc_current_amount' => '0',
+ ];
+ }
+ $this->amounts[$id][$accountId]['current_amount'] = bcadd((string) $this->amounts[$id][$accountId]['current_amount'], (string)$item->current_amount);
+ if (null !== $this->amounts[$id][$accountId]['pc_current_amount'] && null !== $item->native_current_amount) {
+ $this->amounts[$id][$accountId]['pc_current_amount'] = bcadd($this->amounts[$id][$accountId]['pc_current_amount'], (string)$item->native_current_amount);
+ }
+ }
+
+ // get account currency preference for ALL.
+ $set = AccountMeta::whereIn('account_id', array_values($this->accountIds))->where('name', 'currency_id')->get();
+
+ /** @var AccountMeta $item */
+ foreach ($set as $item) {
+ $accountId = (int)$item->account_id;
+ $currencyId = (int)$item->data;
+ if (!array_key_exists($currencyId, $this->currencies)) {
+ $this->currencies[$currencyId] = Amount::getTransactionCurrencyById($currencyId);
+ }
+ // $this->accountCurrencies[$accountId] = $this->currencies[$currencyId];
+ }
+
+ // get account info.
+ $set = Account::whereIn('id', array_values($this->accountIds))->get();
+
+ /** @var Account $item */
+ foreach ($set as $item) {
+ $id = (int)$item->id;
+ $this->accounts[$id] = [
+ 'id' => $id,
+ 'name' => $item->name,
+ ];
+ }
+ }
+
private function collectNotes(): void
{
$notes = Note::query()->whereIn('noteable_id', $this->ids)
@@ -264,8 +266,6 @@ class PiggyBankEnrichment implements EnrichmentInterface
}
}
- private function collectCurrentAmounts(): void {}
-
/**
* Returns the suggested amount the user should save per month, or "".
*/
diff --git a/app/Support/JsonApi/Enrichments/PiggyBankEventEnrichment.php b/app/Support/JsonApi/Enrichments/PiggyBankEventEnrichment.php
index fad6293f90..56a0714466 100644
--- a/app/Support/JsonApi/Enrichments/PiggyBankEventEnrichment.php
+++ b/app/Support/JsonApi/Enrichments/PiggyBankEventEnrichment.php
@@ -38,16 +38,16 @@ use Illuminate\Support\Facades\Log;
class PiggyBankEventEnrichment implements EnrichmentInterface
{
- private User $user; // @phpstan-ignore-line
- private UserGroup $userGroup; // @phpstan-ignore-line
+ private array $accountCurrencies = []; // @phpstan-ignore-line
+ private array $accountIds = []; // @phpstan-ignore-line
private Collection $collection;
+ private array $currencies = [];
+ private array $groupIds = [];
private array $ids = [];
private array $journalIds = [];
- private array $groupIds = [];
- private array $accountIds = [];
private array $piggyBankIds = [];
- private array $accountCurrencies = [];
- private array $currencies = [];
+ private User $user;
+ private UserGroup $userGroup;
// private bool $convertToPrimary = false;
// private TransactionCurrency $primaryCurrency;
@@ -86,6 +86,30 @@ class PiggyBankEventEnrichment implements EnrichmentInterface
$this->userGroup = $userGroup;
}
+ private function appendCollectedData(): void
+ {
+ $this->collection = $this->collection->map(function (PiggyBankEvent $item) {
+ $id = (int)$item->id;
+ $piggyId = (int)$item->piggy_bank_id;
+ $journalId = (int)$item->transaction_journal_id;
+ $currency = null;
+ if (array_key_exists($piggyId, $this->accountIds)) {
+ $accountId = $this->accountIds[$piggyId];
+ if (array_key_exists($accountId, $this->accountCurrencies)) {
+ $currency = $this->accountCurrencies[$accountId];
+ }
+ }
+ $meta = [
+ 'transaction_group_id' => array_key_exists($journalId, $this->groupIds) ? (string)$this->groupIds[$journalId] : null,
+ 'currency' => $currency,
+ ];
+ $item->meta = $meta;
+
+ return $item;
+ });
+
+ }
+
private function collectIds(): void
{
/** @var PiggyBankEvent $event */
@@ -125,28 +149,4 @@ class PiggyBankEventEnrichment implements EnrichmentInterface
$this->accountCurrencies[$accountId] = $this->currencies[$currencyId];
}
}
-
- private function appendCollectedData(): void
- {
- $this->collection = $this->collection->map(function (PiggyBankEvent $item) {
- $id = (int)$item->id;
- $piggyId = (int)$item->piggy_bank_id;
- $journalId = (int)$item->transaction_journal_id;
- $currency = null;
- if (array_key_exists($piggyId, $this->accountIds)) {
- $accountId = $this->accountIds[$piggyId];
- if (array_key_exists($accountId, $this->accountCurrencies)) {
- $currency = $this->accountCurrencies[$accountId];
- }
- }
- $meta = [
- 'transaction_group_id' => array_key_exists($journalId, $this->groupIds) ? (string)$this->groupIds[$journalId] : null,
- 'currency' => $currency,
- ];
- $item->meta = $meta;
-
- return $item;
- });
-
- }
}
diff --git a/app/Support/JsonApi/Enrichments/RecurringEnrichment.php b/app/Support/JsonApi/Enrichments/RecurringEnrichment.php
index c8a3d8707a..7fdfc1c5dc 100644
--- a/app/Support/JsonApi/Enrichments/RecurringEnrichment.php
+++ b/app/Support/JsonApi/Enrichments/RecurringEnrichment.php
@@ -56,25 +56,25 @@ use function Safe\json_decode;
class RecurringEnrichment implements EnrichmentInterface
{
- private Collection $collection;
- private array $ids = [];
+ private array $accounts = [];
+ private Collection $collection;
// private array $transactionTypeIds = [];
// private array $transactionTypes = [];
- private array $notes = [];
- private array $repetitions = [];
- private array $transactions = [];
- private User $user;
- private UserGroup $userGroup;
- private string $language = 'en_US';
- private array $currencyIds = [];
- private array $foreignCurrencyIds = [];
- private array $sourceAccountIds = [];
- private array $destinationAccountIds = [];
- private array $accounts = [];
- private array $currencies = [];
- private array $recurrenceIds = [];
+ private bool $convertToPrimary = false;
+ private array $currencies = [];
+ private array $currencyIds = [];
+ private array $destinationAccountIds = [];
+ private array $foreignCurrencyIds = [];
+ private array $ids = [];
+ private string $language = 'en_US';
+ private array $notes = [];
private readonly TransactionCurrency $primaryCurrency;
- private bool $convertToPrimary = false;
+ private array $recurrenceIds = [];
+ private array $repetitions = [];
+ private array $sourceAccountIds = [];
+ private array $transactions = [];
+ private User $user;
+ private UserGroup $userGroup;
public function __construct()
{
@@ -107,139 +107,6 @@ class RecurringEnrichment implements EnrichmentInterface
return $collection->first();
}
- public function setUser(User $user): void
- {
- $this->user = $user;
- $this->setUserGroup($user->userGroup);
- $this->getLanguage();
- }
-
- public function setUserGroup(UserGroup $userGroup): void
- {
- $this->userGroup = $userGroup;
- }
-
- private function collectIds(): void
- {
- /** @var Recurrence $recurrence */
- foreach ($this->collection as $recurrence) {
- $id = (int)$recurrence->id;
- // $typeId = (int)$recurrence->transaction_type_id;
- $this->ids[] = $id;
- // $this->transactionTypeIds[$id] = $typeId;
- }
- $this->ids = array_unique($this->ids);
-
- // collect transaction types.
- // $transactionTypes = TransactionType::whereIn('id', array_unique($this->transactionTypeIds))->get();
- // foreach ($transactionTypes as $transactionType) {
- // $id = (int)$transactionType->id;
- // $this->transactionTypes[$id] = TransactionTypeEnum::from($transactionType->type);
- // }
- }
-
- private function collectRepetitions(): void
- {
- Log::debug('Start of enrichment: collectRepetitions()');
- $repository = app(RecurringRepositoryInterface::class);
- $repository->setUserGroup($this->userGroup);
- $set = RecurrenceRepetition::whereIn('recurrence_id', $this->ids)->get();
-
- /** @var RecurrenceRepetition $repetition */
- foreach ($set as $repetition) {
- $recurrence = $this->collection->filter(fn (Recurrence $item) => (int)$item->id === (int)$repetition->recurrence_id)->first();
- $fromDate = clone ($recurrence->latest_date ?? $recurrence->first_date);
- $id = (int)$repetition->recurrence_id;
- $repId = (int)$repetition->id;
- $this->repetitions[$id] ??= [];
-
- // get the (future) occurrences for this specific type of repetition:
- $amount = 'daily' === $repetition->repetition_type ? 9 : 5;
- $set = $repository->getXOccurrencesSince($repetition, $fromDate, now(config('app.timezone')), $amount);
- $occurrences = [];
-
- /** @var Carbon $carbon */
- foreach ($set as $carbon) {
- $occurrences[] = $carbon->toAtomString();
- }
- $this->repetitions[$id][$repId] = [
- 'id' => (string)$repId,
- 'created_at' => $repetition->created_at->toAtomString(),
- 'updated_at' => $repetition->updated_at->toAtomString(),
- 'type' => $repetition->repetition_type,
- 'moment' => (string)$repetition->repetition_moment,
- 'skip' => (int)$repetition->repetition_skip,
- 'weekend' => RecurrenceRepetitionWeekend::from((int)$repetition->weekend)->value,
- 'description' => $this->getRepetitionDescription($repetition),
- 'occurrences' => $occurrences,
- ];
- }
- Log::debug('End of enrichment: collectRepetitions()');
- }
-
- private function collectTransactions(): void
- {
- $set = RecurrenceTransaction::whereIn('recurrence_id', $this->ids)->get();
-
- /** @var RecurrenceTransaction $transaction */
- foreach ($set as $transaction) {
- $id = (int)$transaction->recurrence_id;
- $transactionId = (int)$transaction->id;
- $this->recurrenceIds[$transactionId] = $id;
- $this->transactions[$id] ??= [];
- $amount = $transaction->amount;
- $foreignAmount = $transaction->foreign_amount;
-
- $this->transactions[$id][$transactionId] = [
- 'id' => (string)$transactionId,
- // 'recurrence_id' => $id,
- 'transaction_currency_id' => (int)$transaction->transaction_currency_id,
- 'foreign_currency_id' => null === $transaction->foreign_currency_id ? null : (int)$transaction->foreign_currency_id,
- 'source_id' => (int)$transaction->source_id,
- 'object_has_currency_setting' => true,
- 'destination_id' => (int)$transaction->destination_id,
- 'amount' => $amount,
- 'foreign_amount' => $foreignAmount,
- 'pc_amount' => null,
- 'pc_foreign_amount' => null,
- 'description' => $transaction->description,
- 'tags' => [],
- 'category_id' => null,
- 'category_name' => null,
- 'budget_id' => null,
- 'budget_name' => null,
- 'piggy_bank_id' => null,
- 'piggy_bank_name' => null,
- 'subscription_id' => null,
- 'subscription_name' => null,
-
- ];
- // collect all kinds of meta data to be collected later.
- $this->currencyIds[$transactionId] = (int)$transaction->transaction_currency_id;
- $this->sourceAccountIds[$transactionId] = (int)$transaction->source_id;
- $this->destinationAccountIds[$transactionId] = (int)$transaction->destination_id;
- if (null !== $transaction->foreign_currency_id) {
- $this->foreignCurrencyIds[$transactionId] = (int)$transaction->foreign_currency_id;
- }
- }
- }
-
- private function appendCollectedData(): void
- {
- $this->collection = $this->collection->map(function (Recurrence $item) {
- $id = (int)$item->id;
- $meta = [
- 'notes' => $this->notes[$id] ?? null,
- 'repetitions' => array_values($this->repetitions[$id] ?? []),
- 'transactions' => $this->processTransactions(array_values($this->transactions[$id] ?? [])),
- ];
-
- $item->meta = $meta;
-
- return $item;
- });
- }
-
/**
* Parse the repetition in a string that is user readable.
* TODO duplicate with repository.
@@ -290,96 +157,32 @@ class RecurringEnrichment implements EnrichmentInterface
return '';
}
- private function getLanguage(): void
+ public function setUser(User $user): void
{
- /** @var Preference $preference */
- $preference = Preferences::getForUser($this->user, 'language', config('firefly.default_language', 'en_US'));
- $language = $preference->data;
- if (is_array($language)) {
- $language = 'en_US';
- }
- $language = (string)$language;
- $this->language = $language;
+ $this->user = $user;
+ $this->setUserGroup($user->userGroup);
+ $this->getLanguage();
}
- private function collectCurrencies(): void
+ public function setUserGroup(UserGroup $userGroup): void
{
- $all = array_merge(array_unique($this->currencyIds), array_unique($this->foreignCurrencyIds));
- $currencies = TransactionCurrency::whereIn('id', array_unique($all))->get();
- foreach ($currencies as $currency) {
- $id = (int)$currency->id;
- $this->currencies[$id] = $currency;
- }
+ $this->userGroup = $userGroup;
}
- private function processTransactions(array $transactions): array
+ private function appendCollectedData(): void
{
- $return = [];
- $converter = new ExchangeRateConverter();
- foreach ($transactions as $transaction) {
- $currencyId = $transaction['transaction_currency_id'];
- $pcAmount = null;
- $pcForeignAmount = null;
- // set the same amount in the primary currency, if both are the same anyway.
- if (true === $this->convertToPrimary && $currencyId === (int)$this->primaryCurrency->id) {
- $pcAmount = $transaction['amount'];
- }
- // convert the amount to the primary currency, if it is not the same.
- if (true === $this->convertToPrimary && $currencyId !== (int)$this->primaryCurrency->id) {
- $pcAmount = $converter->convert($this->currencies[$currencyId], $this->primaryCurrency, today(), $transaction['amount']);
- }
- if (null !== $transaction['foreign_amount'] && null !== $transaction['foreign_currency_id']) {
- $foreignCurrencyId = $transaction['foreign_currency_id'];
- if ($foreignCurrencyId !== $this->primaryCurrency->id) {
- $pcForeignAmount = $converter->convert($this->currencies[$foreignCurrencyId], $this->primaryCurrency, today(), $transaction['foreign_amount']);
- }
- }
+ $this->collection = $this->collection->map(function (Recurrence $item) {
+ $id = (int)$item->id;
+ $meta = [
+ 'notes' => $this->notes[$id] ?? null,
+ 'repetitions' => array_values($this->repetitions[$id] ?? []),
+ 'transactions' => $this->processTransactions(array_values($this->transactions[$id] ?? [])),
+ ];
- $transaction['pc_amount'] = $pcAmount;
- $transaction['pc_foreign_amount'] = $pcForeignAmount;
+ $item->meta = $meta;
- $sourceId = $transaction['source_id'];
- $transaction['source_name'] = $this->accounts[$sourceId]->name;
- $transaction['source_iban'] = $this->accounts[$sourceId]->iban;
- $transaction['source_type'] = $this->accounts[$sourceId]->accountType->type;
- $transaction['source_id'] = (string)$transaction['source_id'];
-
- $destId = $transaction['destination_id'];
- $transaction['destination_name'] = $this->accounts[$destId]->name;
- $transaction['destination_iban'] = $this->accounts[$destId]->iban;
- $transaction['destination_type'] = $this->accounts[$destId]->accountType->type;
- $transaction['destination_id'] = (string)$transaction['destination_id'];
-
- $transaction['currency_id'] = (string)$currencyId;
- $transaction['currency_name'] = $this->currencies[$currencyId]->name;
- $transaction['currency_code'] = $this->currencies[$currencyId]->code;
- $transaction['currency_symbol'] = $this->currencies[$currencyId]->symbol;
- $transaction['currency_decimal_places'] = $this->currencies[$currencyId]->decimal_places;
-
- $transaction['primary_currency_id'] = (string)$this->primaryCurrency->id;
- $transaction['primary_currency_name'] = $this->primaryCurrency->name;
- $transaction['primary_currency_code'] = $this->primaryCurrency->code;
- $transaction['primary_currency_symbol'] = $this->primaryCurrency->symbol;
- $transaction['primary_currency_decimal_places'] = $this->primaryCurrency->decimal_places;
-
- // $transaction['foreign_currency_id'] = null;
- $transaction['foreign_currency_name'] = null;
- $transaction['foreign_currency_code'] = null;
- $transaction['foreign_currency_symbol'] = null;
- $transaction['foreign_currency_decimal_places'] = null;
- if (null !== $transaction['foreign_currency_id']) {
- $currencyId = $transaction['foreign_currency_id'];
- $transaction['foreign_currency_id'] = (string)$currencyId;
- $transaction['foreign_currency_name'] = $this->currencies[$currencyId]->name;
- $transaction['foreign_currency_code'] = $this->currencies[$currencyId]->code;
- $transaction['foreign_currency_symbol'] = $this->currencies[$currencyId]->symbol;
- $transaction['foreign_currency_decimal_places'] = $this->currencies[$currencyId]->decimal_places;
- }
- unset($transaction['transaction_currency_id']);
- $return[] = $transaction;
- }
-
- return $return;
+ return $item;
+ });
}
private function collectAccounts(): void
@@ -394,6 +197,180 @@ class RecurringEnrichment implements EnrichmentInterface
}
}
+ private function collectBillInfo(array $billIds): void
+ {
+ if (0 === count($billIds)) {
+ return;
+ }
+ $ids = Arr::pluck($billIds, 'bill_id');
+ $bills = Bill::whereIn('id', $ids)->get();
+ $mapped = [];
+ foreach ($bills as $bill) {
+ $mapped[(int)$bill->id] = $bill;
+ }
+ foreach ($billIds as $info) {
+ $recurrenceId = $info['recurrence_id'];
+ $transactionId = $info['transaction_id'];
+ $this->transactions[$recurrenceId][$transactionId]['subscription_name'] = $mapped[$info['bill_id']]->name ?? '';
+ }
+ }
+
+ private function collectBudgetInfo(array $budgetIds): void
+ {
+ if (0 === count($budgetIds)) {
+ return;
+ }
+ $ids = Arr::pluck($budgetIds, 'budget_id');
+ $categories = Budget::whereIn('id', $ids)->get();
+ $mapped = [];
+ foreach ($categories as $category) {
+ $mapped[(int)$category->id] = $category;
+ }
+ foreach ($budgetIds as $info) {
+ $recurrenceId = $info['recurrence_id'];
+ $transactionId = $info['transaction_id'];
+ $this->transactions[$recurrenceId][$transactionId]['budget_name'] = $mapped[$info['budget_id']]->name ?? '';
+ }
+ }
+
+ private function collectCategoryIdInfo(array $categoryIds): void
+ {
+ if (0 === count($categoryIds)) {
+ return;
+ }
+ $ids = Arr::pluck($categoryIds, 'category_id');
+ $categories = Category::whereIn('id', $ids)->get();
+ $mapped = [];
+ foreach ($categories as $category) {
+ $mapped[(int)$category->id] = $category;
+ }
+ foreach ($categoryIds as $info) {
+ $recurrenceId = $info['recurrence_id'];
+ $transactionId = $info['transaction_id'];
+ $this->transactions[$recurrenceId][$transactionId]['category_name'] = $mapped[$info['category_id']]->name ?? '';
+ }
+ }
+
+ /**
+ * TODO This method does look-up in a loop.
+ */
+ private function collectCategoryNameInfo(array $categoryNames): void
+ {
+ if (0 === count($categoryNames)) {
+ return;
+ }
+ $factory = app(CategoryFactory::class);
+ $factory->setUser($this->user);
+ foreach ($categoryNames as $info) {
+ $recurrenceId = $info['recurrence_id'];
+ $transactionId = $info['transaction_id'];
+ $category = $factory->findOrCreate(null, $info['category_name']);
+ if (null !== $category) {
+ $this->transactions[$recurrenceId][$transactionId]['category_id'] = (string)$category->id;
+ $this->transactions[$recurrenceId][$transactionId]['category_name'] = $category->name;
+ }
+ }
+ }
+
+ private function collectCurrencies(): void
+ {
+ $all = array_merge(array_unique($this->currencyIds), array_unique($this->foreignCurrencyIds));
+ $currencies = TransactionCurrency::whereIn('id', array_unique($all))->get();
+ foreach ($currencies as $currency) {
+ $id = (int)$currency->id;
+ $this->currencies[$id] = $currency;
+ }
+ }
+
+ private function collectIds(): void
+ {
+ /** @var Recurrence $recurrence */
+ foreach ($this->collection as $recurrence) {
+ $id = (int)$recurrence->id;
+ // $typeId = (int)$recurrence->transaction_type_id;
+ $this->ids[] = $id;
+ // $this->transactionTypeIds[$id] = $typeId;
+ }
+ $this->ids = array_unique($this->ids);
+
+ // collect transaction types.
+ // $transactionTypes = TransactionType::whereIn('id', array_unique($this->transactionTypeIds))->get();
+ // foreach ($transactionTypes as $transactionType) {
+ // $id = (int)$transactionType->id;
+ // $this->transactionTypes[$id] = TransactionTypeEnum::from($transactionType->type);
+ // }
+ }
+
+ private function collectNotes(): void
+ {
+ $notes = Note::query()->whereIn('noteable_id', $this->ids)
+ ->whereNotNull('notes.text')
+ ->where('notes.text', '!=', '')
+ ->where('noteable_type', Recurrence::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
+ ;
+ foreach ($notes as $note) {
+ $this->notes[(int)$note['noteable_id']] = (string)$note['text'];
+ }
+ Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
+ }
+
+ private function collectPiggyBankInfo(array $piggyBankIds): void
+ {
+ if (0 === count($piggyBankIds)) {
+ return;
+ }
+ $ids = Arr::pluck($piggyBankIds, 'piggy_bank_id');
+ $piggyBanks = PiggyBank::whereIn('id', $ids)->get();
+ $mapped = [];
+ foreach ($piggyBanks as $piggyBank) {
+ $mapped[(int)$piggyBank->id] = $piggyBank;
+ }
+ foreach ($piggyBankIds as $info) {
+ $recurrenceId = $info['recurrence_id'];
+ $transactionId = $info['transaction_id'];
+ $this->transactions[$recurrenceId][$transactionId]['piggy_bank_name'] = $mapped[$info['piggy_bank_id']]->name ?? '';
+ }
+ }
+
+ private function collectRepetitions(): void
+ {
+ Log::debug('Start of enrichment: collectRepetitions()');
+ $repository = app(RecurringRepositoryInterface::class);
+ $repository->setUserGroup($this->userGroup);
+ $set = RecurrenceRepetition::whereIn('recurrence_id', $this->ids)->get();
+
+ /** @var RecurrenceRepetition $repetition */
+ foreach ($set as $repetition) {
+ $recurrence = $this->collection->filter(fn (Recurrence $item) => (int)$item->id === (int)$repetition->recurrence_id)->first();
+ $fromDate = clone ($recurrence->latest_date ?? $recurrence->first_date);
+ $id = (int)$repetition->recurrence_id;
+ $repId = (int)$repetition->id;
+ $this->repetitions[$id] ??= [];
+
+ // get the (future) occurrences for this specific type of repetition:
+ $amount = 'daily' === $repetition->repetition_type ? 9 : 5;
+ $set = $repository->getXOccurrencesSince($repetition, $fromDate, now(config('app.timezone')), $amount);
+ $occurrences = [];
+
+ /** @var Carbon $carbon */
+ foreach ($set as $carbon) {
+ $occurrences[] = $carbon->toAtomString();
+ }
+ $this->repetitions[$id][$repId] = [
+ 'id' => (string)$repId,
+ 'created_at' => $repetition->created_at->toAtomString(),
+ 'updated_at' => $repetition->updated_at->toAtomString(),
+ 'type' => $repetition->repetition_type,
+ 'moment' => (string)$repetition->repetition_moment,
+ 'skip' => (int)$repetition->repetition_skip,
+ 'weekend' => RecurrenceRepetitionWeekend::from((int)$repetition->weekend)->value,
+ 'description' => $this->getRepetitionDescription($repetition),
+ 'occurrences' => $occurrences,
+ ];
+ }
+ Log::debug('End of enrichment: collectRepetitions()');
+ }
+
private function collectTransactionMetaData(): void
{
$ids = array_keys($this->transactions);
@@ -504,109 +481,132 @@ class RecurringEnrichment implements EnrichmentInterface
$this->collectBudgetInfo($budgetIds);
}
- private function collectBillInfo(array $billIds): void
+ private function collectTransactions(): void
{
- if (0 === count($billIds)) {
- return;
- }
- $ids = Arr::pluck($billIds, 'bill_id');
- $bills = Bill::whereIn('id', $ids)->get();
- $mapped = [];
- foreach ($bills as $bill) {
- $mapped[(int)$bill->id] = $bill;
- }
- foreach ($billIds as $info) {
- $recurrenceId = $info['recurrence_id'];
- $transactionId = $info['transaction_id'];
- $this->transactions[$recurrenceId][$transactionId]['subscription_name'] = $mapped[$info['bill_id']]->name ?? '';
- }
- }
+ $set = RecurrenceTransaction::whereIn('recurrence_id', $this->ids)->get();
- private function collectPiggyBankInfo(array $piggyBankIds): void
- {
- if (0 === count($piggyBankIds)) {
- return;
- }
- $ids = Arr::pluck($piggyBankIds, 'piggy_bank_id');
- $piggyBanks = PiggyBank::whereIn('id', $ids)->get();
- $mapped = [];
- foreach ($piggyBanks as $piggyBank) {
- $mapped[(int)$piggyBank->id] = $piggyBank;
- }
- foreach ($piggyBankIds as $info) {
- $recurrenceId = $info['recurrence_id'];
- $transactionId = $info['transaction_id'];
- $this->transactions[$recurrenceId][$transactionId]['piggy_bank_name'] = $mapped[$info['piggy_bank_id']]->name ?? '';
- }
- }
+ /** @var RecurrenceTransaction $transaction */
+ foreach ($set as $transaction) {
+ $id = (int)$transaction->recurrence_id;
+ $transactionId = (int)$transaction->id;
+ $this->recurrenceIds[$transactionId] = $id;
+ $this->transactions[$id] ??= [];
+ $amount = $transaction->amount;
+ $foreignAmount = $transaction->foreign_amount;
- private function collectCategoryIdInfo(array $categoryIds): void
- {
- if (0 === count($categoryIds)) {
- return;
- }
- $ids = Arr::pluck($categoryIds, 'category_id');
- $categories = Category::whereIn('id', $ids)->get();
- $mapped = [];
- foreach ($categories as $category) {
- $mapped[(int)$category->id] = $category;
- }
- foreach ($categoryIds as $info) {
- $recurrenceId = $info['recurrence_id'];
- $transactionId = $info['transaction_id'];
- $this->transactions[$recurrenceId][$transactionId]['category_name'] = $mapped[$info['category_id']]->name ?? '';
- }
- }
+ $this->transactions[$id][$transactionId] = [
+ 'id' => (string)$transactionId,
+ // 'recurrence_id' => $id,
+ 'transaction_currency_id' => (int)$transaction->transaction_currency_id,
+ 'foreign_currency_id' => null === $transaction->foreign_currency_id ? null : (int)$transaction->foreign_currency_id,
+ 'source_id' => (int)$transaction->source_id,
+ 'object_has_currency_setting' => true,
+ 'destination_id' => (int)$transaction->destination_id,
+ 'amount' => $amount,
+ 'foreign_amount' => $foreignAmount,
+ 'pc_amount' => null,
+ 'pc_foreign_amount' => null,
+ 'description' => $transaction->description,
+ 'tags' => [],
+ 'category_id' => null,
+ 'category_name' => null,
+ 'budget_id' => null,
+ 'budget_name' => null,
+ 'piggy_bank_id' => null,
+ 'piggy_bank_name' => null,
+ 'subscription_id' => null,
+ 'subscription_name' => null,
- /**
- * TODO This method does look-up in a loop.
- */
- private function collectCategoryNameInfo(array $categoryNames): void
- {
- if (0 === count($categoryNames)) {
- return;
- }
- $factory = app(CategoryFactory::class);
- $factory->setUser($this->user);
- foreach ($categoryNames as $info) {
- $recurrenceId = $info['recurrence_id'];
- $transactionId = $info['transaction_id'];
- $category = $factory->findOrCreate(null, $info['category_name']);
- if (null !== $category) {
- $this->transactions[$recurrenceId][$transactionId]['category_id'] = (string)$category->id;
- $this->transactions[$recurrenceId][$transactionId]['category_name'] = $category->name;
+ ];
+ // collect all kinds of meta data to be collected later.
+ $this->currencyIds[$transactionId] = (int)$transaction->transaction_currency_id;
+ $this->sourceAccountIds[$transactionId] = (int)$transaction->source_id;
+ $this->destinationAccountIds[$transactionId] = (int)$transaction->destination_id;
+ if (null !== $transaction->foreign_currency_id) {
+ $this->foreignCurrencyIds[$transactionId] = (int)$transaction->foreign_currency_id;
}
}
}
- private function collectBudgetInfo(array $budgetIds): void
+ private function getLanguage(): void
{
- if (0 === count($budgetIds)) {
- return;
- }
- $ids = Arr::pluck($budgetIds, 'budget_id');
- $categories = Budget::whereIn('id', $ids)->get();
- $mapped = [];
- foreach ($categories as $category) {
- $mapped[(int)$category->id] = $category;
- }
- foreach ($budgetIds as $info) {
- $recurrenceId = $info['recurrence_id'];
- $transactionId = $info['transaction_id'];
- $this->transactions[$recurrenceId][$transactionId]['budget_name'] = $mapped[$info['budget_id']]->name ?? '';
+ /** @var Preference $preference */
+ $preference = Preferences::getForUser($this->user, 'language', config('firefly.default_language', 'en_US'));
+ $language = $preference->data;
+ if (is_array($language)) {
+ $language = 'en_US';
}
+ $language = (string)$language;
+ $this->language = $language;
}
- private function collectNotes(): void
+ private function processTransactions(array $transactions): array
{
- $notes = Note::query()->whereIn('noteable_id', $this->ids)
- ->whereNotNull('notes.text')
- ->where('notes.text', '!=', '')
- ->where('noteable_type', Recurrence::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
- ;
- foreach ($notes as $note) {
- $this->notes[(int)$note['noteable_id']] = (string)$note['text'];
+ $return = [];
+ $converter = new ExchangeRateConverter();
+ foreach ($transactions as $transaction) {
+ $currencyId = $transaction['transaction_currency_id'];
+ $pcAmount = null;
+ $pcForeignAmount = null;
+ // set the same amount in the primary currency, if both are the same anyway.
+ if (true === $this->convertToPrimary && $currencyId === (int)$this->primaryCurrency->id) {
+ $pcAmount = $transaction['amount'];
+ }
+ // convert the amount to the primary currency, if it is not the same.
+ if (true === $this->convertToPrimary && $currencyId !== (int)$this->primaryCurrency->id) {
+ $pcAmount = $converter->convert($this->currencies[$currencyId], $this->primaryCurrency, today(), $transaction['amount']);
+ }
+ if (null !== $transaction['foreign_amount'] && null !== $transaction['foreign_currency_id']) {
+ $foreignCurrencyId = $transaction['foreign_currency_id'];
+ if ($foreignCurrencyId !== $this->primaryCurrency->id) {
+ $pcForeignAmount = $converter->convert($this->currencies[$foreignCurrencyId], $this->primaryCurrency, today(), $transaction['foreign_amount']);
+ }
+ }
+
+ $transaction['pc_amount'] = $pcAmount;
+ $transaction['pc_foreign_amount'] = $pcForeignAmount;
+
+ $sourceId = $transaction['source_id'];
+ $transaction['source_name'] = $this->accounts[$sourceId]->name;
+ $transaction['source_iban'] = $this->accounts[$sourceId]->iban;
+ $transaction['source_type'] = $this->accounts[$sourceId]->accountType->type;
+ $transaction['source_id'] = (string)$transaction['source_id'];
+
+ $destId = $transaction['destination_id'];
+ $transaction['destination_name'] = $this->accounts[$destId]->name;
+ $transaction['destination_iban'] = $this->accounts[$destId]->iban;
+ $transaction['destination_type'] = $this->accounts[$destId]->accountType->type;
+ $transaction['destination_id'] = (string)$transaction['destination_id'];
+
+ $transaction['currency_id'] = (string)$currencyId;
+ $transaction['currency_name'] = $this->currencies[$currencyId]->name;
+ $transaction['currency_code'] = $this->currencies[$currencyId]->code;
+ $transaction['currency_symbol'] = $this->currencies[$currencyId]->symbol;
+ $transaction['currency_decimal_places'] = $this->currencies[$currencyId]->decimal_places;
+
+ $transaction['primary_currency_id'] = (string)$this->primaryCurrency->id;
+ $transaction['primary_currency_name'] = $this->primaryCurrency->name;
+ $transaction['primary_currency_code'] = $this->primaryCurrency->code;
+ $transaction['primary_currency_symbol'] = $this->primaryCurrency->symbol;
+ $transaction['primary_currency_decimal_places'] = $this->primaryCurrency->decimal_places;
+
+ // $transaction['foreign_currency_id'] = null;
+ $transaction['foreign_currency_name'] = null;
+ $transaction['foreign_currency_code'] = null;
+ $transaction['foreign_currency_symbol'] = null;
+ $transaction['foreign_currency_decimal_places'] = null;
+ if (null !== $transaction['foreign_currency_id']) {
+ $currencyId = $transaction['foreign_currency_id'];
+ $transaction['foreign_currency_id'] = (string)$currencyId;
+ $transaction['foreign_currency_name'] = $this->currencies[$currencyId]->name;
+ $transaction['foreign_currency_code'] = $this->currencies[$currencyId]->code;
+ $transaction['foreign_currency_symbol'] = $this->currencies[$currencyId]->symbol;
+ $transaction['foreign_currency_decimal_places'] = $this->currencies[$currencyId]->decimal_places;
+ }
+ unset($transaction['transaction_currency_id']);
+ $return[] = $transaction;
}
- Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
+
+ return $return;
}
}
diff --git a/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php b/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php
index 06388a80bc..68c3dc0f12 100644
--- a/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php
+++ b/app/Support/JsonApi/Enrichments/SubscriptionEnrichment.php
@@ -46,20 +46,20 @@ use Illuminate\Support\Facades\Log;
class SubscriptionEnrichment implements EnrichmentInterface
{
- private User $user;
- private UserGroup $userGroup; // @phpstan-ignore-line
- private Collection $collection;
+ private BillDateCalculator $calculator;
+ private Collection $collection; // @phpstan-ignore-line
private readonly bool $convertToPrimary;
- private ?Carbon $start = null;
- private ?Carbon $end = null;
- private array $subscriptionIds = [];
- private array $objectGroups = [];
- private array $mappedObjects = [];
- private array $paidDates = [];
- private array $notes = [];
- private array $payDates = [];
+ private ?Carbon $end = null;
+ private array $mappedObjects = [];
+ private array $notes = [];
+ private array $objectGroups = [];
+ private array $paidDates = [];
+ private array $payDates = [];
private readonly TransactionCurrency $primaryCurrency;
- private BillDateCalculator $calculator;
+ private ?Carbon $start = null;
+ private array $subscriptionIds = [];
+ private User $user;
+ private UserGroup $userGroup;
public function __construct()
{
@@ -151,17 +151,14 @@ class SubscriptionEnrichment implements EnrichmentInterface
return $collection->first();
}
- private function collectNotes(): void
+ public function setEnd(?Carbon $end): void
{
- $notes = Note::query()->whereIn('noteable_id', $this->subscriptionIds)
- ->whereNotNull('notes.text')
- ->where('notes.text', '!=', '')
- ->where('noteable_type', Bill::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
- ;
- foreach ($notes as $note) {
- $this->notes[(int)$note['noteable_id']] = (string)$note['text'];
- }
- Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
+ $this->end = $end;
+ }
+
+ public function setStart(?Carbon $start): void
+ {
+ $this->start = $start;
}
public function setUser(User $user): void
@@ -175,13 +172,40 @@ class SubscriptionEnrichment implements EnrichmentInterface
$this->userGroup = $userGroup;
}
- private function collectSubscriptionIds(): void
+ /**
+ * Returns the latest date in the set, or start when set is empty.
+ */
+ protected function lastPaidDate(Bill $subscription, Collection $dates, Carbon $default): Carbon
{
- /** @var Bill $bill */
- foreach ($this->collection as $bill) {
- $this->subscriptionIds[] = (int)$bill->id;
+ $filtered = $dates->filter(fn (TransactionJournal $journal) => (int)$journal->bill_id === (int)$subscription->id);
+ Log::debug(sprintf('Filtered down from %d to %d entries for bill #%d.', $dates->count(), $filtered->count(), $subscription->id));
+ if (0 === $filtered->count()) {
+ return $default;
}
- $this->subscriptionIds = array_unique($this->subscriptionIds);
+
+ $latest = $filtered->first()->date;
+
+ /** @var TransactionJournal $journal */
+ foreach ($filtered as $journal) {
+ if ($journal->date->gte($latest)) {
+ $latest = $journal->date;
+ }
+ }
+
+ return $latest;
+ }
+
+ private function collectNotes(): void
+ {
+ $notes = Note::query()->whereIn('noteable_id', $this->subscriptionIds)
+ ->whereNotNull('notes.text')
+ ->where('notes.text', '!=', '')
+ ->where('noteable_type', Bill::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
+ ;
+ foreach ($notes as $note) {
+ $this->notes[(int)$note['noteable_id']] = (string)$note['text'];
+ }
+ Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
}
private function collectObjectGroups(): void
@@ -329,63 +353,6 @@ class SubscriptionEnrichment implements EnrichmentInterface
}
- public function setStart(?Carbon $start): void
- {
- $this->start = $start;
- }
-
- public function setEnd(?Carbon $end): void
- {
- $this->end = $end;
- }
-
- /**
- * Returns the latest date in the set, or start when set is empty.
- */
- protected function lastPaidDate(Bill $subscription, Collection $dates, Carbon $default): Carbon
- {
- $filtered = $dates->filter(fn (TransactionJournal $journal) => (int)$journal->bill_id === (int)$subscription->id);
- Log::debug(sprintf('Filtered down from %d to %d entries for bill #%d.', $dates->count(), $filtered->count(), $subscription->id));
- if (0 === $filtered->count()) {
- return $default;
- }
-
- $latest = $filtered->first()->date;
-
- /** @var TransactionJournal $journal */
- foreach ($filtered as $journal) {
- if ($journal->date->gte($latest)) {
- $latest = $journal->date;
- }
- }
-
- return $latest;
- }
-
- private function getLastPaidDate(array $paidData): ?Carbon
- {
- // Log::debug('getLastPaidDate()');
- $return = null;
- foreach ($paidData as $entry) {
- if (null !== $return) {
- /** @var Carbon $current */
- $current = $entry['date_object'];
- if ($current->gt($return)) {
- $return = clone $current;
- }
- Log::debug(sprintf('[a] Last paid date is: %s', $return->format('Y-m-d')));
- }
- if (null === $return) {
- /** @var Carbon $return */
- $return = $entry['date_object'];
- Log::debug(sprintf('[b] Last paid date is: %s', $return->format('Y-m-d')));
- }
- }
- // Log::debug(sprintf('[c] Last paid date is: "%s"', $return?->format('Y-m-d')));
-
- return $return;
- }
-
private function collectPayDates(): void
{
if (!$this->start instanceof Carbon || !$this->end instanceof Carbon) {
@@ -411,6 +378,15 @@ class SubscriptionEnrichment implements EnrichmentInterface
}
}
+ private function collectSubscriptionIds(): void
+ {
+ /** @var Bill $bill */
+ foreach ($this->collection as $bill) {
+ $this->subscriptionIds[] = (int)$bill->id;
+ }
+ $this->subscriptionIds = array_unique($this->subscriptionIds);
+ }
+
private function filterPaidDates(array $entries): array
{
return array_map(function (array $entry) {
@@ -420,6 +396,30 @@ class SubscriptionEnrichment implements EnrichmentInterface
}, $entries);
}
+ private function getLastPaidDate(array $paidData): ?Carbon
+ {
+ // Log::debug('getLastPaidDate()');
+ $return = null;
+ foreach ($paidData as $entry) {
+ if (null !== $return) {
+ /** @var Carbon $current */
+ $current = $entry['date_object'];
+ if ($current->gt($return)) {
+ $return = clone $current;
+ }
+ Log::debug(sprintf('[a] Last paid date is: %s', $return->format('Y-m-d')));
+ }
+ if (null === $return) {
+ /** @var Carbon $return */
+ $return = $entry['date_object'];
+ Log::debug(sprintf('[b] Last paid date is: %s', $return->format('Y-m-d')));
+ }
+ }
+ // Log::debug(sprintf('[c] Last paid date is: "%s"', $return?->format('Y-m-d')));
+
+ return $return;
+ }
+
private function getNextExpectedMatch(array $payDates): ?Carbon
{
// next expected match
diff --git a/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php b/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php
index d322331135..a8dccfe9fc 100644
--- a/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php
+++ b/app/Support/JsonApi/Enrichments/TransactionGroupEnrichment.php
@@ -45,17 +45,17 @@ use Override;
class TransactionGroupEnrichment implements EnrichmentInterface
{
- private array $attachmentCount = [];
- private Collection $collection;
- private readonly array $dateFields;
- private array $journalIds = [];
- private array $locations = [];
- private array $metaData = [];
- private array $notes = [];
- private array $tags = [];
- private User $user; // @phpstan-ignore-line
+ private array $attachmentCount = [];
+ private Collection $collection;
+ private readonly array $dateFields;
+ private array $journalIds = [];
+ private array $locations = [];
+ private array $metaData = [];
+ private array $notes = [];
private readonly TransactionCurrency $primaryCurrency;
- private UserGroup $userGroup; // @phpstan-ignore-line
+ private array $tags = []; // @phpstan-ignore-line
+ private User $user;
+ private UserGroup $userGroup; // @phpstan-ignore-line
public function __construct()
{
@@ -63,20 +63,6 @@ class TransactionGroupEnrichment implements EnrichmentInterface
$this->primaryCurrency = Amount::getPrimaryCurrency();
}
- #[Override]
- public function enrichSingle(array|Model $model): array|TransactionGroup
- {
- Log::debug(__METHOD__);
- if (is_array($model)) {
- $collection = new Collection()->push($model);
- $collection = $this->enrich($collection);
-
- return $collection->first();
- }
-
- throw new FireflyException('Cannot enrich single model.');
- }
-
#[Override]
public function enrich(Collection $collection): Collection
{
@@ -96,93 +82,29 @@ class TransactionGroupEnrichment implements EnrichmentInterface
return $this->collection;
}
- private function collectJournalIds(): void
+ #[Override]
+ public function enrichSingle(array|Model $model): array|TransactionGroup
{
- /** @var array $group */
- foreach ($this->collection as $group) {
- foreach ($group['transactions'] as $journal) {
- $this->journalIds[] = $journal['transaction_journal_id'];
- }
+ Log::debug(__METHOD__);
+ if (is_array($model)) {
+ $collection = new Collection()->push($model);
+ $collection = $this->enrich($collection);
+
+ return $collection->first();
}
- $this->journalIds = array_unique($this->journalIds);
+
+ throw new FireflyException('Cannot enrich single model.');
}
- private function collectNotes(): void
+ public function setUser(User $user): void
{
- $notes = Note::query()->whereIn('noteable_id', $this->journalIds)
- ->whereNotNull('notes.text')
- ->where('notes.text', '!=', '')
- ->where('noteable_type', TransactionJournal::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
- ;
- foreach ($notes as $note) {
- $this->notes[(int) $note['noteable_id']] = (string) $note['text'];
- }
- Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
+ $this->user = $user;
+ $this->userGroup = $user->userGroup;
}
- private function collectTags(): void
+ public function setUserGroup(UserGroup $userGroup): void
{
- $set = Tag::leftJoin('tag_transaction_journal', 'tags.id', '=', 'tag_transaction_journal.tag_id')
- ->whereIn('tag_transaction_journal.transaction_journal_id', $this->journalIds)
- ->get(['tag_transaction_journal.transaction_journal_id', 'tags.tag'])->toArray()
- ;
- foreach ($set as $item) {
- $journalId = $item['transaction_journal_id'];
- $this->tags[$journalId] ??= [];
- $this->tags[$journalId][] = $item['tag'];
- }
- }
-
- private function collectMetaData(): void
- {
- $set = TransactionJournalMeta::whereIn('transaction_journal_id', $this->journalIds)->get(['transaction_journal_id', 'name', 'data'])->toArray();
- foreach ($set as $entry) {
- $name = $entry['name'];
- $data = (string) $entry['data'];
- if ('' === $data) {
- continue;
- }
- if (in_array($name, $this->dateFields, true)) {
- // Log::debug(sprintf('Meta data for "%s" is a date : "%s"', $name, $data));
- $this->metaData[$entry['transaction_journal_id']][$name] = Carbon::parse($data, config('app.timezone'));
- // Log::debug(sprintf('Meta data for "%s" converts to: "%s"', $name, $this->metaData[$entry['transaction_journal_id']][$name]->toW3CString()));
-
- continue;
- }
- $this->metaData[(int) $entry['transaction_journal_id']][$name] = $data;
- }
- }
-
- private function collectLocations(): void
- {
- $locations = Location::query()->whereIn('locatable_id', $this->journalIds)
- ->where('locatable_type', TransactionJournal::class)->get(['locations.locatable_id', 'locations.latitude', 'locations.longitude', 'locations.zoom_level'])->toArray()
- ;
- foreach ($locations as $location) {
- $this->locations[(int) $location['locatable_id']]
- = [
- 'latitude' => (float) $location['latitude'],
- 'longitude' => (float) $location['longitude'],
- 'zoom_level' => (int) $location['zoom_level'],
- ];
- }
- Log::debug(sprintf('Enrich with %d locations(s)', count($this->locations)));
- }
-
- private function collectAttachmentCount(): void
- {
- // select count(id) as nr_of_attachments, attachable_id from attachments
- // group by attachable_id
- $attachments = Attachment::query()
- ->whereIn('attachable_id', $this->journalIds)
- ->where('attachable_type', TransactionJournal::class)
- ->groupBy('attachable_id')
- ->get(['attachable_id', DB::raw('COUNT(id) as nr_of_attachments')])
- ->toArray()
- ;
- foreach ($attachments as $row) {
- $this->attachmentCount[(int) $row['attachable_id']] = (int) $row['nr_of_attachments'];
- }
+ $this->userGroup = $userGroup;
}
private function appendCollectedData(): void
@@ -196,7 +118,7 @@ class TransactionGroupEnrichment implements EnrichmentInterface
$this->collection = $this->collection->map(function (array $item) use ($primaryCurrency, $notes, $tags, $metaData, $locations, $attachmentCount) {
foreach ($item['transactions'] as $index => $transaction) {
- $journalId = (int) $transaction['transaction_journal_id'];
+ $journalId = (int)$transaction['transaction_journal_id'];
// attach notes if they exist:
$item['transactions'][$index]['notes'] = array_key_exists($journalId, $notes) ? $notes[$journalId] : null;
@@ -216,11 +138,11 @@ class TransactionGroupEnrichment implements EnrichmentInterface
// primary currency
$item['transactions'][$index]['primary_currency'] = [
- 'id' => (string) $primaryCurrency->id,
- 'code' => $primaryCurrency->code,
- 'name' => $primaryCurrency->name,
- 'symbol' => $primaryCurrency->symbol,
- 'decimal_places' => $primaryCurrency->decimal_places,
+ 'id' => (string)$primaryCurrency->id,
+ 'code' => $primaryCurrency->code,
+ 'name' => $primaryCurrency->name,
+ 'symbol' => $primaryCurrency->symbol,
+ 'decimal_places' => $primaryCurrency->decimal_places,
];
// append meta data
@@ -248,14 +170,92 @@ class TransactionGroupEnrichment implements EnrichmentInterface
});
}
- public function setUser(User $user): void
+ private function collectAttachmentCount(): void
{
- $this->user = $user;
- $this->userGroup = $user->userGroup;
+ // select count(id) as nr_of_attachments, attachable_id from attachments
+ // group by attachable_id
+ $attachments = Attachment::query()
+ ->whereIn('attachable_id', $this->journalIds)
+ ->where('attachable_type', TransactionJournal::class)
+ ->groupBy('attachable_id')
+ ->get(['attachable_id', DB::raw('COUNT(id) as nr_of_attachments')])
+ ->toArray()
+ ;
+ foreach ($attachments as $row) {
+ $this->attachmentCount[(int)$row['attachable_id']] = (int)$row['nr_of_attachments'];
+ }
}
- public function setUserGroup(UserGroup $userGroup): void
+ private function collectJournalIds(): void
{
- $this->userGroup = $userGroup;
+ /** @var array $group */
+ foreach ($this->collection as $group) {
+ foreach ($group['transactions'] as $journal) {
+ $this->journalIds[] = $journal['transaction_journal_id'];
+ }
+ }
+ $this->journalIds = array_unique($this->journalIds);
+ }
+
+ private function collectLocations(): void
+ {
+ $locations = Location::query()->whereIn('locatable_id', $this->journalIds)
+ ->where('locatable_type', TransactionJournal::class)->get(['locations.locatable_id', 'locations.latitude', 'locations.longitude', 'locations.zoom_level'])->toArray()
+ ;
+ foreach ($locations as $location) {
+ $this->locations[(int)$location['locatable_id']]
+ = [
+ 'latitude' => (float)$location['latitude'],
+ 'longitude' => (float)$location['longitude'],
+ 'zoom_level' => (int)$location['zoom_level'],
+ ];
+ }
+ Log::debug(sprintf('Enrich with %d locations(s)', count($this->locations)));
+ }
+
+ private function collectMetaData(): void
+ {
+ $set = TransactionJournalMeta::whereIn('transaction_journal_id', $this->journalIds)->get(['transaction_journal_id', 'name', 'data'])->toArray();
+ foreach ($set as $entry) {
+ $name = $entry['name'];
+ $data = (string)$entry['data'];
+ if ('' === $data) {
+ continue;
+ }
+ if (in_array($name, $this->dateFields, true)) {
+ // Log::debug(sprintf('Meta data for "%s" is a date : "%s"', $name, $data));
+ $this->metaData[$entry['transaction_journal_id']][$name] = Carbon::parse($data, config('app.timezone'));
+ // Log::debug(sprintf('Meta data for "%s" converts to: "%s"', $name, $this->metaData[$entry['transaction_journal_id']][$name]->toW3CString()));
+
+ continue;
+ }
+ $this->metaData[(int)$entry['transaction_journal_id']][$name] = $data;
+ }
+ }
+
+ private function collectNotes(): void
+ {
+ $notes = Note::query()->whereIn('noteable_id', $this->journalIds)
+ ->whereNotNull('notes.text')
+ ->where('notes.text', '!=', '')
+ ->where('noteable_type', TransactionJournal::class)->get(['notes.noteable_id', 'notes.text'])->toArray()
+ ;
+ foreach ($notes as $note) {
+ $this->notes[(int)$note['noteable_id']] = (string)$note['text'];
+ }
+ Log::debug(sprintf('Enrich with %d note(s)', count($this->notes)));
+ }
+
+ private function collectTags(): void
+ {
+ $set = Tag::leftJoin('tag_transaction_journal', 'tags.id', '=', 'tag_transaction_journal.tag_id')
+ ->whereIn('tag_transaction_journal.transaction_journal_id', $this->journalIds)
+ ->get(['tag_transaction_journal.transaction_journal_id', 'tags.tag'])->toArray()
+ ;
+ foreach ($set as $item) {
+ $journalId = $item['transaction_journal_id'];
+ $this->tags[$journalId] ??= [];
+ $this->tags[$journalId][] = $item['tag'];
+ }
}
}
diff --git a/app/Support/JsonApi/Enrichments/WebhookEnrichment.php b/app/Support/JsonApi/Enrichments/WebhookEnrichment.php
index 516705892a..e847a16b0f 100644
--- a/app/Support/JsonApi/Enrichments/WebhookEnrichment.php
+++ b/app/Support/JsonApi/Enrichments/WebhookEnrichment.php
@@ -43,16 +43,15 @@ use stdClass;
class WebhookEnrichment implements EnrichmentInterface
{
private Collection $collection;
- private User $user; // @phpstan-ignore-line
- private UserGroup $userGroup; // @phpstan-ignore-line
- private array $ids = [];
- private array $deliveries = [];
- private array $responses = [];
- private array $triggers = [];
-
- private array $webhookDeliveries = [];
- private array $webhookResponses = [];
- private array $webhookTriggers = [];
+ private array $deliveries = []; // @phpstan-ignore-line
+ private array $ids = []; // @phpstan-ignore-line
+ private array $responses = [];
+ private array $triggers = [];
+ private User $user;
+ private UserGroup $userGroup;
+ private array $webhookDeliveries = [];
+ private array $webhookResponses = [];
+ private array $webhookTriggers = [];
public function enrich(Collection $collection): Collection
{
@@ -86,6 +85,20 @@ class WebhookEnrichment implements EnrichmentInterface
$this->userGroup = $userGroup;
}
+ private function appendCollectedInfo(): void
+ {
+ $this->collection = $this->collection->map(function (Webhook $item) {
+ $meta = [
+ 'deliveries' => $this->webhookDeliveries[$item->id] ?? [],
+ 'responses' => $this->webhookResponses[$item->id] ?? [],
+ 'triggers' => $this->webhookTriggers[$item->id] ?? [],
+ ];
+ $item->meta = $meta;
+
+ return $item;
+ });
+ }
+
private function collectIds(): void
{
/** @var Webhook $webhook */
@@ -147,18 +160,4 @@ class WebhookEnrichment implements EnrichmentInterface
$this->webhookTriggers[$id][] = WebhookTriggerEnum::from($this->triggers[$triggerId])->name;
}
}
-
- private function appendCollectedInfo(): void
- {
- $this->collection = $this->collection->map(function (Webhook $item) {
- $meta = [
- 'deliveries' => $this->webhookDeliveries[$item->id] ?? [],
- 'responses' => $this->webhookResponses[$item->id] ?? [],
- 'triggers' => $this->webhookTriggers[$item->id] ?? [],
- ];
- $item->meta = $meta;
-
- return $item;
- });
- }
}
diff --git a/app/Support/Models/AccountBalanceCalculator.php b/app/Support/Models/AccountBalanceCalculator.php
index d2a3572b7d..5675f92adc 100644
--- a/app/Support/Models/AccountBalanceCalculator.php
+++ b/app/Support/Models/AccountBalanceCalculator.php
@@ -62,6 +62,47 @@ class AccountBalanceCalculator
$object->optimizedCalculation(new Collection());
}
+ public static function recalculateForJournal(TransactionJournal $transactionJournal): void
+ {
+ Log::debug(__METHOD__);
+ $object = new self();
+
+ $set = [];
+ foreach ($transactionJournal->transactions as $transaction) {
+ $set[$transaction->account_id] = $transaction->account;
+ }
+ $accounts = new Collection()->push(...$set);
+ $object->optimizedCalculation($accounts, $transactionJournal->date);
+ }
+
+ private function getLatestBalance(int $accountId, int $currencyId, ?Carbon $notBefore): string
+ {
+ if (!$notBefore instanceof Carbon) {
+ return '0';
+ }
+ Log::debug(sprintf('getLatestBalance: notBefore date is "%s", calculating', $notBefore->format('Y-m-d')));
+ $query = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
+ ->whereNull('transactions.deleted_at')
+ ->where('transaction_journals.transaction_currency_id', $currencyId)
+ ->whereNull('transaction_journals.deleted_at')
+ // this order is the same as GroupCollector
+ ->orderBy('transaction_journals.date', 'DESC')
+ ->orderBy('transaction_journals.order', 'ASC')
+ ->orderBy('transaction_journals.id', 'DESC')
+ ->orderBy('transaction_journals.description', 'DESC')
+ ->orderBy('transactions.amount', 'DESC')
+ ->where('transactions.account_id', $accountId)
+ ;
+ $notBefore->startOfDay();
+ $query->where('transaction_journals.date', '<', $notBefore);
+
+ $first = $query->first(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount', 'transactions.balance_after']);
+ $balance = (string)($first->balance_after ?? '0');
+ Log::debug(sprintf('getLatestBalance: found balance: %s in transaction #%d', $balance, $first->id ?? 0));
+
+ return $balance;
+ }
+
private function optimizedCalculation(Collection $accounts, ?Carbon $notBefore = null): void
{
Log::debug('start of optimizedCalculation');
@@ -123,34 +164,6 @@ class AccountBalanceCalculator
$this->storeAccountBalances($balances);
}
- private function getLatestBalance(int $accountId, int $currencyId, ?Carbon $notBefore): string
- {
- if (!$notBefore instanceof Carbon) {
- return '0';
- }
- Log::debug(sprintf('getLatestBalance: notBefore date is "%s", calculating', $notBefore->format('Y-m-d')));
- $query = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
- ->whereNull('transactions.deleted_at')
- ->where('transaction_journals.transaction_currency_id', $currencyId)
- ->whereNull('transaction_journals.deleted_at')
- // this order is the same as GroupCollector
- ->orderBy('transaction_journals.date', 'DESC')
- ->orderBy('transaction_journals.order', 'ASC')
- ->orderBy('transaction_journals.id', 'DESC')
- ->orderBy('transaction_journals.description', 'DESC')
- ->orderBy('transactions.amount', 'DESC')
- ->where('transactions.account_id', $accountId)
- ;
- $notBefore->startOfDay();
- $query->where('transaction_journals.date', '<', $notBefore);
-
- $first = $query->first(['transactions.id', 'transactions.balance_dirty', 'transactions.transaction_currency_id', 'transaction_journals.date', 'transactions.account_id', 'transactions.amount', 'transactions.balance_after']);
- $balance = (string)($first->balance_after ?? '0');
- Log::debug(sprintf('getLatestBalance: found balance: %s in transaction #%d', $balance, $first->id ?? 0));
-
- return $balance;
- }
-
private function storeAccountBalances(array $balances): void
{
/**
@@ -196,17 +209,4 @@ class AccountBalanceCalculator
}
}
}
-
- public static function recalculateForJournal(TransactionJournal $transactionJournal): void
- {
- Log::debug(__METHOD__);
- $object = new self();
-
- $set = [];
- foreach ($transactionJournal->transactions as $transaction) {
- $set[$transaction->account_id] = $transaction->account;
- }
- $accounts = new Collection()->push(...$set);
- $object->optimizedCalculation($accounts, $transactionJournal->date);
- }
}
diff --git a/app/Support/Models/ReturnsIntegerIdTrait.php b/app/Support/Models/ReturnsIntegerIdTrait.php
index 804b9be384..f3fac096ce 100644
--- a/app/Support/Models/ReturnsIntegerIdTrait.php
+++ b/app/Support/Models/ReturnsIntegerIdTrait.php
@@ -39,7 +39,7 @@ trait ReturnsIntegerIdTrait
protected function id(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (int)$value,
);
}
}
diff --git a/app/Support/Models/ReturnsIntegerUserIdTrait.php b/app/Support/Models/ReturnsIntegerUserIdTrait.php
index a0d2bc79e9..3f1808923d 100644
--- a/app/Support/Models/ReturnsIntegerUserIdTrait.php
+++ b/app/Support/Models/ReturnsIntegerUserIdTrait.php
@@ -37,14 +37,14 @@ trait ReturnsIntegerUserIdTrait
protected function userGroupId(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (int)$value,
);
}
protected function userId(): Attribute
{
return Attribute::make(
- get: static fn ($value) => (int) $value,
+ get: static fn ($value) => (int)$value,
);
}
}
diff --git a/app/Support/Navigation.php b/app/Support/Navigation.php
index d88b01ba38..cb9ac29d9f 100644
--- a/app/Support/Navigation.php
+++ b/app/Support/Navigation.php
@@ -88,24 +88,6 @@ class Navigation
return $this->nextDateByInterval($date, $functionMap[$repeatFreq], $skip);
}
- public function nextDateByInterval(Carbon $epoch, Periodicity $periodicity, int $skipInterval = 0): Carbon
- {
- try {
- return $this->calculator->nextDateByInterval($epoch, $periodicity, $skipInterval);
- } catch (IntervalException $exception) {
- Log::warning($exception->getMessage(), ['exception' => $exception]);
- } catch (Throwable $exception) {
- Log::error($exception->getMessage(), ['exception' => $exception]);
- }
-
- Log::debug(
- 'Any error occurred to calculate the next date.',
- ['date' => $epoch, 'periodicity' => $periodicity->name, 'skipInterval' => $skipInterval]
- );
-
- return $epoch;
- }
-
public function blockPeriods(Carbon $start, Carbon $end, string $range): array
{
if ($end < $start) {
@@ -159,6 +141,421 @@ class Navigation
return $periods;
}
+ public function daysUntilEndOfMonth(Carbon $date): int
+ {
+ $endOfMonth = $date->copy()->endOfMonth();
+
+ return (int)$date->diffInDays($endOfMonth, true);
+ }
+
+ public function diffInPeriods(string $period, int $skip, Carbon $beginning, Carbon $end): int
+ {
+ Log::debug(sprintf(
+ 'diffInPeriods: %s (skip: %d), between %s and %s.',
+ $period,
+ $skip,
+ $beginning->format('Y-m-d'),
+ $end->format('Y-m-d')
+ ));
+ $map = [
+ 'daily' => 'diffInDays',
+ 'weekly' => 'diffInWeeks',
+ 'monthly' => 'diffInMonths',
+ 'quarterly' => 'diffInMonths',
+ 'half-year' => 'diffInMonths',
+ 'yearly' => 'diffInYears',
+ ];
+ if (!array_key_exists($period, $map)) {
+ Log::warning(sprintf('No diffInPeriods for period "%s"', $period));
+
+ return 1;
+ }
+ $func = $map[$period];
+ // first do the diff
+ $floatDiff = $beginning->{$func}($end, true); // @phpstan-ignore-line
+
+ // then correct for quarterly or half-year
+ if ('quarterly' === $period) {
+ Log::debug(sprintf('Q: Corrected %f to %f', $floatDiff, $floatDiff / 3));
+ $floatDiff /= 3;
+ }
+ if ('half-year' === $period) {
+ Log::debug(sprintf('H: Corrected %f to %f', $floatDiff, $floatDiff / 6));
+ $floatDiff /= 6;
+ }
+
+ // then do ceil()
+ $diff = ceil($floatDiff);
+
+ Log::debug(sprintf('Diff is %f periods (%d rounded up)', $floatDiff, $diff));
+
+ if ($skip > 0) {
+ $parameter = $skip + 1;
+ $diff = ceil($diff / $parameter) * $parameter;
+ Log::debug(sprintf(
+ 'diffInPeriods: skip is %d, so param is %d, and diff becomes %d',
+ $skip,
+ $parameter,
+ $diff
+ ));
+ }
+
+ return (int)$diff;
+ }
+
+ public function endOfPeriod(Carbon $end, string $repeatFreq): Carbon
+ {
+ $currentEnd = clone $end;
+ // Log::debug(sprintf('Now in endOfPeriod("%s", "%s").', $currentEnd->toIso8601String(), $repeatFreq));
+
+ $functionMap = [
+ '1D' => 'endOfDay',
+ 'daily' => 'endOfDay',
+ '1W' => 'addWeek',
+ 'week' => 'addWeek',
+ 'weekly' => 'addWeek',
+ '1M' => 'addMonth',
+ 'month' => 'addMonth',
+ 'monthly' => 'addMonth',
+ '3M' => 'addQuarter',
+ 'quarter' => 'addQuarter',
+ 'quarterly' => 'addQuarter',
+ '6M' => 'addMonths',
+ 'half-year' => 'addMonths',
+ 'half_year' => 'addMonths',
+ 'year' => 'addYear',
+ 'yearly' => 'addYear',
+ '1Y' => 'addYear',
+ ];
+ $modifierMap = ['half-year' => 6, 'half_year' => 6, '6M' => 6];
+ $subDay = ['week', 'weekly', '1W', 'month', 'monthly', '1M', '3M', 'quarter', 'quarterly', '6M', 'half-year', 'half_year', '1Y', 'year', 'yearly'];
+
+ if ('custom' === $repeatFreq) {
+ // if the repeat frequency is "custom", use the current session start/end to see how large the range is,
+ // and use that to "add" another period.
+ // if there is no session data available use "30 days" as a default.
+ $diffInDays = 30;
+ if (null !== session('start') && null !== session('end')) {
+ Log::debug('Session data available.');
+
+ /** @var Carbon $tStart */
+ $tStart = session('start', today(config('app.timezone'))->startOfMonth());
+
+ /** @var Carbon $tEnd */
+ $tEnd = session('end', today(config('app.timezone'))->endOfMonth());
+ $diffInDays = (int)$tStart->diffInDays($tEnd, true);
+ }
+ Log::debug(sprintf('Diff in days is %d', $diffInDays));
+ $currentEnd->addDays($diffInDays);
+
+ return $currentEnd;
+ }
+ if ('MTD' === $repeatFreq) {
+ $today = today();
+ if ($today->isSameMonth($end)) {
+ return $today->endOfDay()->milli(0);
+ }
+
+ return $end->endOfMonth();
+ }
+
+ $result = match ($repeatFreq) {
+ 'last7' => $currentEnd->addDays(7)->startOfDay(),
+ 'last30' => $currentEnd->addDays(30)->startOfDay(),
+ 'last90' => $currentEnd->addDays(90)->startOfDay(),
+ 'last365' => $currentEnd->addDays(365)->startOfDay(),
+ 'MTD' => $currentEnd->startOfMonth()->startOfDay(),
+ 'QTD' => $currentEnd->firstOfQuarter()->startOfDay(),
+ 'YTD' => $currentEnd->startOfYear()->startOfDay(),
+ default => null,
+ };
+ if (null !== $result) {
+ return $result;
+ }
+ unset($result);
+
+ if (!array_key_exists($repeatFreq, $functionMap)) {
+ Log::error(sprintf('Cannot do endOfPeriod for $repeat_freq "%s"', $repeatFreq));
+
+ return $end;
+ }
+ $function = $functionMap[$repeatFreq];
+
+ if (array_key_exists($repeatFreq, $modifierMap)) {
+ $currentEnd->{$function}($modifierMap[$repeatFreq])->milli(0); // @phpstan-ignore-line
+ if (in_array($repeatFreq, $subDay, true)) {
+ $currentEnd->subDay();
+ }
+ $currentEnd->endOfDay()->milli(0);
+
+ return $currentEnd;
+ }
+ $currentEnd->{$function}(); // @phpstan-ignore-line
+ $currentEnd->endOfDay()->milli(0);
+ if (in_array($repeatFreq, $subDay, true)) {
+ $currentEnd->subDay();
+ }
+ // Log::debug(sprintf('Final result: %s', $currentEnd->toIso8601String()));
+
+ return $currentEnd;
+ }
+
+ public function endOfX(Carbon $theCurrentEnd, string $repeatFreq, ?Carbon $maxDate): Carbon
+ {
+ $functionMap = [
+ '1D' => 'endOfDay',
+ 'daily' => 'endOfDay',
+ '1W' => 'endOfWeek',
+ 'week' => 'endOfWeek',
+ 'weekly' => 'endOfWeek',
+ 'month' => 'endOfMonth',
+ '1M' => 'endOfMonth',
+ 'monthly' => 'endOfMonth',
+ '3M' => 'lastOfQuarter',
+ 'quarter' => 'lastOfQuarter',
+ 'quarterly' => 'lastOfQuarter',
+ '1Y' => 'endOfYear',
+ 'year' => 'endOfYear',
+ 'yearly' => 'endOfYear',
+ ];
+
+ $currentEnd = clone $theCurrentEnd;
+
+ if (array_key_exists($repeatFreq, $functionMap)) {
+ $function = $functionMap[$repeatFreq];
+ $currentEnd->{$function}(); // @phpstan-ignore-line
+ }
+
+ if ($maxDate instanceof Carbon && $currentEnd > $maxDate) {
+ return clone $maxDate;
+ }
+
+ return $currentEnd;
+ }
+
+ /**
+ * Returns the user's view range and if necessary, corrects the dynamic view
+ * range to a normal range.
+ */
+ public function getViewRange(bool $correct): string
+ {
+ $range = app('preferences')->get('viewRange', '1M')->data ?? '1M';
+ if (is_array($range)) {
+ $range = '1M';
+ }
+ $range = (string)$range;
+ if (!$correct) {
+ return $range;
+ }
+
+ return match ($range) {
+ 'last7' => '1W',
+ 'last30', 'MTD' => '1M',
+ 'last90', 'QTD' => '3M',
+ 'last365', 'YTD' => '1Y',
+ default => $range,
+ };
+ }
+
+ /**
+ * @throws FireflyException
+ */
+ public function listOfPeriods(Carbon $start, Carbon $end): array
+ {
+ $locale = app('steam')->getLocale();
+ // define period to increment
+ $increment = 'addDay';
+ $format = $this->preferredCarbonFormat($start, $end);
+ $displayFormat = (string)trans('config.month_and_day_js', [], $locale);
+ $diff = $start->diffInMonths($end, true);
+ // increment by month (for year)
+ if ($diff >= 1.0001 && $diff < 12.001) {
+ $increment = 'addMonth';
+ $displayFormat = (string)trans('config.month_js');
+ }
+
+ // increment by year (for multi-year)
+ if ($diff >= 12.0001) {
+ $increment = 'addYear';
+ $displayFormat = (string)trans('config.year_js');
+ }
+ $begin = clone $start;
+ $entries = [];
+ while ($begin < $end) {
+ $formatted = $begin->format($format);
+ $displayed = $begin->isoFormat($displayFormat);
+ $entries[$formatted] = $displayed;
+ $begin->{$increment}(); // @phpstan-ignore-line
+ }
+
+ return $entries;
+ }
+
+ public function nextDateByInterval(Carbon $epoch, Periodicity $periodicity, int $skipInterval = 0): Carbon
+ {
+ try {
+ return $this->calculator->nextDateByInterval($epoch, $periodicity, $skipInterval);
+ } catch (IntervalException $exception) {
+ Log::warning($exception->getMessage(), ['exception' => $exception]);
+ } catch (Throwable $exception) {
+ Log::error($exception->getMessage(), ['exception' => $exception]);
+ }
+
+ Log::debug(
+ 'Any error occurred to calculate the next date.',
+ ['date' => $epoch, 'periodicity' => $periodicity->name, 'skipInterval' => $skipInterval]
+ );
+
+ return $epoch;
+ }
+
+ public function periodShow(Carbon $theDate, string $repeatFrequency): string
+ {
+ $date = clone $theDate;
+ $formatMap = [
+ '1D' => (string)trans('config.specific_day_js'),
+ 'daily' => (string)trans('config.specific_day_js'),
+ 'custom' => (string)trans('config.specific_day_js'),
+ '1W' => (string)trans('config.week_in_year_js'),
+ 'week' => (string)trans('config.week_in_year_js'),
+ 'weekly' => (string)trans('config.week_in_year_js'),
+ '1M' => (string)trans('config.month_js'),
+ 'month' => (string)trans('config.month_js'),
+ 'monthly' => (string)trans('config.month_js'),
+ '1Y' => (string)trans('config.year_js'),
+ 'year' => (string)trans('config.year_js'),
+ 'yearly' => (string)trans('config.year_js'),
+ '6M' => (string)trans('config.half_year_js'),
+ ];
+
+ if (array_key_exists($repeatFrequency, $formatMap)) {
+ return $date->isoFormat($formatMap[$repeatFrequency]);
+ }
+ if ('3M' === $repeatFrequency || 'quarter' === $repeatFrequency) {
+ $quarter = ceil($theDate->month / 3);
+
+ return sprintf('Q%d %d', $quarter, $theDate->year);
+ }
+
+ // special formatter for quarter of year
+ Log::error(sprintf('No date formats for frequency "%s"!', $repeatFrequency));
+
+ throw new FireflyException(sprintf('No date formats for frequency "%s"!', $repeatFrequency));
+
+ return $date->format('Y-m-d');
+ }
+
+ /**
+ * If the date difference between start and end is less than a month, method returns "Y-m-d". If the difference is
+ * less than a year, method returns "Y-m". If the date difference is larger, method returns "Y".
+ */
+ public function preferredCarbonFormat(Carbon $start, Carbon $end): string
+ {
+ $format = 'Y-m-d';
+ $diff = $start->diffInMonths($end, true);
+ // Log::debug(sprintf('preferredCarbonFormat(%s, %s) = %f', $start->format('Y-m-d'), $end->format('Y-m-d'), $diff));
+ if ($diff >= 1.001 && $diff < 12.001) {
+ // Log::debug(sprintf('Return Y-m because %s', $diff));
+ $format = 'Y-m';
+ }
+
+ if ($diff >= 12.001) {
+ // Log::debug(sprintf('Return Y because %s', $diff));
+ return 'Y';
+ }
+
+ return $format;
+ }
+
+ /**
+ * Same as preferredCarbonFormat but by string
+ */
+ public function preferredCarbonFormatByPeriod(string $period): string
+ {
+ return match ($period) {
+ default => 'Y-m-d',
+ // '1D' => 'Y-m-d',
+ '1W' => '\WW,Y',
+ '1M' => 'Y-m',
+ '3M', '6M' => '\QQ,Y',
+ '1Y' => 'Y',
+ };
+ }
+
+ /**
+ * If the date difference between start and end is less than a month, method returns trans(config.month_and_day).
+ * If the difference is less than a year, method returns "config.month". If the date difference is larger, method
+ * returns "config.year".
+ */
+ public function preferredCarbonLocalizedFormat(Carbon $start, Carbon $end): string
+ {
+ $locale = app('steam')->getLocale();
+ $diff = $start->diffInMonths($end, true);
+ if ($diff >= 1.001 && $diff < 12.001) {
+ return (string)trans('config.month_js', [], $locale);
+ }
+
+ if ($diff >= 12.001) {
+ return (string)trans('config.year_js', [], $locale);
+ }
+
+ return (string)trans('config.month_and_day_js', [], $locale);
+ }
+
+ /**
+ * If the date difference between start and end is less than a month, method returns "endOfDay". If the difference
+ * is less than a year, method returns "endOfMonth". If the date difference is larger, method returns "endOfYear".
+ */
+ public function preferredEndOfPeriod(Carbon $start, Carbon $end): string
+ {
+ $diff = $start->diffInMonths($end, true);
+ if ($diff >= 1.001 && $diff < 12.001) {
+ return 'endOfMonth';
+ }
+
+ if ($diff >= 12.001) {
+ return 'endOfYear';
+ }
+
+ return 'endOfDay';
+ }
+
+ /**
+ * If the date difference between start and end is less than a month, method returns "1D". If the difference is
+ * less than a year, method returns "1M". If the date difference is larger, method returns "1Y".
+ */
+ public function preferredRangeFormat(Carbon $start, Carbon $end): string
+ {
+ $diff = $start->diffInMonths($end, true);
+ if ($diff >= 1.001 && $diff < 12.001) {
+ return '1M';
+ }
+
+ if ($diff >= 12.001) {
+ return '1Y';
+ }
+
+ return '1D';
+ }
+
+ /**
+ * If the date difference between start and end is less than a month, method returns "%Y-%m-%d". If the difference
+ * is less than a year, method returns "%Y-%m". If the date difference is larger, method returns "%Y".
+ */
+ public function preferredSqlFormat(Carbon $start, Carbon $end): string
+ {
+ $diff = $start->diffInMonths($end, true);
+ if ($diff >= 1.001 && $diff < 12.001) {
+ return '%Y-%m';
+ }
+
+ if ($diff >= 12.001) {
+ return '%Y';
+ }
+
+ return '%Y-%m-%d';
+ }
+
public function startOfPeriod(Carbon $theDate, string $repeatFreq): Carbon
{
$date = clone $theDate;
@@ -236,401 +633,6 @@ class Navigation
return $theDate;
}
- public function endOfPeriod(Carbon $end, string $repeatFreq): Carbon
- {
- $currentEnd = clone $end;
- // Log::debug(sprintf('Now in endOfPeriod("%s", "%s").', $currentEnd->toIso8601String(), $repeatFreq));
-
- $functionMap = [
- '1D' => 'endOfDay',
- 'daily' => 'endOfDay',
- '1W' => 'addWeek',
- 'week' => 'addWeek',
- 'weekly' => 'addWeek',
- '1M' => 'addMonth',
- 'month' => 'addMonth',
- 'monthly' => 'addMonth',
- '3M' => 'addQuarter',
- 'quarter' => 'addQuarter',
- 'quarterly' => 'addQuarter',
- '6M' => 'addMonths',
- 'half-year' => 'addMonths',
- 'half_year' => 'addMonths',
- 'year' => 'addYear',
- 'yearly' => 'addYear',
- '1Y' => 'addYear',
- ];
- $modifierMap = ['half-year' => 6, 'half_year' => 6, '6M' => 6];
- $subDay = ['week', 'weekly', '1W', 'month', 'monthly', '1M', '3M', 'quarter', 'quarterly', '6M', 'half-year', 'half_year', '1Y', 'year', 'yearly'];
-
- if ('custom' === $repeatFreq) {
- // if the repeat frequency is "custom", use the current session start/end to see how large the range is,
- // and use that to "add" another period.
- // if there is no session data available use "30 days" as a default.
- $diffInDays = 30;
- if (null !== session('start') && null !== session('end')) {
- Log::debug('Session data available.');
-
- /** @var Carbon $tStart */
- $tStart = session('start', today(config('app.timezone'))->startOfMonth());
-
- /** @var Carbon $tEnd */
- $tEnd = session('end', today(config('app.timezone'))->endOfMonth());
- $diffInDays = (int) $tStart->diffInDays($tEnd, true);
- }
- Log::debug(sprintf('Diff in days is %d', $diffInDays));
- $currentEnd->addDays($diffInDays);
-
- return $currentEnd;
- }
- if ('MTD' === $repeatFreq) {
- $today = today();
- if ($today->isSameMonth($end)) {
- return $today->endOfDay();
- }
-
- return $end->endOfMonth();
- }
-
- $result = match ($repeatFreq) {
- 'last7' => $currentEnd->addDays(7)->startOfDay(),
- 'last30' => $currentEnd->addDays(30)->startOfDay(),
- 'last90' => $currentEnd->addDays(90)->startOfDay(),
- 'last365' => $currentEnd->addDays(365)->startOfDay(),
- 'MTD' => $currentEnd->startOfMonth()->startOfDay(),
- 'QTD' => $currentEnd->firstOfQuarter()->startOfDay(),
- 'YTD' => $currentEnd->startOfYear()->startOfDay(),
- default => null,
- };
- if (null !== $result) {
- return $result;
- }
- unset($result);
-
- if (!array_key_exists($repeatFreq, $functionMap)) {
- Log::error(sprintf('Cannot do endOfPeriod for $repeat_freq "%s"', $repeatFreq));
-
- return $end;
- }
- $function = $functionMap[$repeatFreq];
-
- if (array_key_exists($repeatFreq, $modifierMap)) {
- $currentEnd->{$function}($modifierMap[$repeatFreq]); // @phpstan-ignore-line
- if (in_array($repeatFreq, $subDay, true)) {
- $currentEnd->subDay();
- }
- $currentEnd->endOfDay();
-
- return $currentEnd;
- }
- $currentEnd->{$function}(); // @phpstan-ignore-line
- $currentEnd->endOfDay();
- if (in_array($repeatFreq, $subDay, true)) {
- $currentEnd->subDay();
- }
- // Log::debug(sprintf('Final result: %s', $currentEnd->toIso8601String()));
-
- return $currentEnd;
- }
-
- public function daysUntilEndOfMonth(Carbon $date): int
- {
- $endOfMonth = $date->copy()->endOfMonth();
-
- return (int) $date->diffInDays($endOfMonth, true);
- }
-
- public function diffInPeriods(string $period, int $skip, Carbon $beginning, Carbon $end): int
- {
- Log::debug(sprintf(
- 'diffInPeriods: %s (skip: %d), between %s and %s.',
- $period,
- $skip,
- $beginning->format('Y-m-d'),
- $end->format('Y-m-d')
- ));
- $map = [
- 'daily' => 'diffInDays',
- 'weekly' => 'diffInWeeks',
- 'monthly' => 'diffInMonths',
- 'quarterly' => 'diffInMonths',
- 'half-year' => 'diffInMonths',
- 'yearly' => 'diffInYears',
- ];
- if (!array_key_exists($period, $map)) {
- Log::warning(sprintf('No diffInPeriods for period "%s"', $period));
-
- return 1;
- }
- $func = $map[$period];
- // first do the diff
- $floatDiff = $beginning->{$func}($end, true); // @phpstan-ignore-line
-
- // then correct for quarterly or half-year
- if ('quarterly' === $period) {
- Log::debug(sprintf('Q: Corrected %f to %f', $floatDiff, $floatDiff / 3));
- $floatDiff /= 3;
- }
- if ('half-year' === $period) {
- Log::debug(sprintf('H: Corrected %f to %f', $floatDiff, $floatDiff / 6));
- $floatDiff /= 6;
- }
-
- // then do ceil()
- $diff = ceil($floatDiff);
-
- Log::debug(sprintf('Diff is %f periods (%d rounded up)', $floatDiff, $diff));
-
- if ($skip > 0) {
- $parameter = $skip + 1;
- $diff = ceil($diff / $parameter) * $parameter;
- Log::debug(sprintf(
- 'diffInPeriods: skip is %d, so param is %d, and diff becomes %d',
- $skip,
- $parameter,
- $diff
- ));
- }
-
- return (int) $diff;
- }
-
- public function endOfX(Carbon $theCurrentEnd, string $repeatFreq, ?Carbon $maxDate): Carbon
- {
- $functionMap = [
- '1D' => 'endOfDay',
- 'daily' => 'endOfDay',
- '1W' => 'endOfWeek',
- 'week' => 'endOfWeek',
- 'weekly' => 'endOfWeek',
- 'month' => 'endOfMonth',
- '1M' => 'endOfMonth',
- 'monthly' => 'endOfMonth',
- '3M' => 'lastOfQuarter',
- 'quarter' => 'lastOfQuarter',
- 'quarterly' => 'lastOfQuarter',
- '1Y' => 'endOfYear',
- 'year' => 'endOfYear',
- 'yearly' => 'endOfYear',
- ];
-
- $currentEnd = clone $theCurrentEnd;
-
- if (array_key_exists($repeatFreq, $functionMap)) {
- $function = $functionMap[$repeatFreq];
- $currentEnd->{$function}(); // @phpstan-ignore-line
- }
-
- if ($maxDate instanceof Carbon && $currentEnd > $maxDate) {
- return clone $maxDate;
- }
-
- return $currentEnd;
- }
-
- /**
- * Returns the user's view range and if necessary, corrects the dynamic view
- * range to a normal range.
- */
- public function getViewRange(bool $correct): string
- {
- $range = app('preferences')->get('viewRange', '1M')->data ?? '1M';
- if (is_array($range)) {
- $range = '1M';
- }
- $range = (string) $range;
- if (!$correct) {
- return $range;
- }
-
- return match ($range) {
- 'last7' => '1W',
- 'last30', 'MTD' => '1M',
- 'last90', 'QTD' => '3M',
- 'last365', 'YTD' => '1Y',
- default => $range,
- };
- }
-
- /**
- * @throws FireflyException
- */
- public function listOfPeriods(Carbon $start, Carbon $end): array
- {
- $locale = app('steam')->getLocale();
- // define period to increment
- $increment = 'addDay';
- $format = $this->preferredCarbonFormat($start, $end);
- $displayFormat = (string) trans('config.month_and_day_js', [], $locale);
- $diff = $start->diffInMonths($end, true);
- // increment by month (for year)
- if ($diff >= 1.0001 && $diff < 12.001) {
- $increment = 'addMonth';
- $displayFormat = (string) trans('config.month_js');
- }
-
- // increment by year (for multi-year)
- if ($diff >= 12.0001) {
- $increment = 'addYear';
- $displayFormat = (string) trans('config.year_js');
- }
- $begin = clone $start;
- $entries = [];
- while ($begin < $end) {
- $formatted = $begin->format($format);
- $displayed = $begin->isoFormat($displayFormat);
- $entries[$formatted] = $displayed;
- $begin->{$increment}(); // @phpstan-ignore-line
- }
-
- return $entries;
- }
-
- /**
- * If the date difference between start and end is less than a month, method returns "Y-m-d". If the difference is
- * less than a year, method returns "Y-m". If the date difference is larger, method returns "Y".
- */
- public function preferredCarbonFormat(Carbon $start, Carbon $end): string
- {
- $format = 'Y-m-d';
- $diff = $start->diffInMonths($end, true);
- // Log::debug(sprintf('preferredCarbonFormat(%s, %s) = %f', $start->format('Y-m-d'), $end->format('Y-m-d'), $diff));
- if ($diff >= 1.001 && $diff < 12.001) {
- // Log::debug(sprintf('Return Y-m because %s', $diff));
- $format = 'Y-m';
- }
-
- if ($diff >= 12.001) {
- // Log::debug(sprintf('Return Y because %s', $diff));
- return 'Y';
- }
-
- return $format;
- }
-
- public function periodShow(Carbon $theDate, string $repeatFrequency): string
- {
- $date = clone $theDate;
- $formatMap = [
- '1D' => (string) trans('config.specific_day_js'),
- 'daily' => (string) trans('config.specific_day_js'),
- 'custom' => (string) trans('config.specific_day_js'),
- '1W' => (string) trans('config.week_in_year_js'),
- 'week' => (string) trans('config.week_in_year_js'),
- 'weekly' => (string) trans('config.week_in_year_js'),
- '1M' => (string) trans('config.month_js'),
- 'month' => (string) trans('config.month_js'),
- 'monthly' => (string) trans('config.month_js'),
- '1Y' => (string) trans('config.year_js'),
- 'year' => (string) trans('config.year_js'),
- 'yearly' => (string) trans('config.year_js'),
- '6M' => (string) trans('config.half_year_js'),
- ];
-
- if (array_key_exists($repeatFrequency, $formatMap)) {
- return $date->isoFormat($formatMap[$repeatFrequency]);
- }
- if ('3M' === $repeatFrequency || 'quarter' === $repeatFrequency) {
- $quarter = ceil($theDate->month / 3);
-
- return sprintf('Q%d %d', $quarter, $theDate->year);
- }
-
- // special formatter for quarter of year
- Log::error(sprintf('No date formats for frequency "%s"!', $repeatFrequency));
-
- return $date->format('Y-m-d');
- }
-
- /**
- * Same as preferredCarbonFormat but by string
- */
- public function preferredCarbonFormatByPeriod(string $period): string
- {
- return match ($period) {
- default => 'Y-m-d',
- // '1D' => 'Y-m-d',
- '1W' => '\WW,Y',
- '1M' => 'Y-m',
- '3M', '6M' => '\QQ,Y',
- '1Y' => 'Y',
- };
- }
-
- /**
- * If the date difference between start and end is less than a month, method returns trans(config.month_and_day).
- * If the difference is less than a year, method returns "config.month". If the date difference is larger, method
- * returns "config.year".
- */
- public function preferredCarbonLocalizedFormat(Carbon $start, Carbon $end): string
- {
- $locale = app('steam')->getLocale();
- $diff = $start->diffInMonths($end, true);
- if ($diff >= 1.001 && $diff < 12.001) {
- return (string) trans('config.month_js', [], $locale);
- }
-
- if ($diff >= 12.001) {
- return (string) trans('config.year_js', [], $locale);
- }
-
- return (string) trans('config.month_and_day_js', [], $locale);
- }
-
- /**
- * If the date difference between start and end is less than a month, method returns "endOfDay". If the difference
- * is less than a year, method returns "endOfMonth". If the date difference is larger, method returns "endOfYear".
- */
- public function preferredEndOfPeriod(Carbon $start, Carbon $end): string
- {
- $diff = $start->diffInMonths($end, true);
- if ($diff >= 1.001 && $diff < 12.001) {
- return 'endOfMonth';
- }
-
- if ($diff >= 12.001) {
- return 'endOfYear';
- }
-
- return 'endOfDay';
- }
-
- /**
- * If the date difference between start and end is less than a month, method returns "1D". If the difference is
- * less than a year, method returns "1M". If the date difference is larger, method returns "1Y".
- */
- public function preferredRangeFormat(Carbon $start, Carbon $end): string
- {
- $diff = $start->diffInMonths($end, true);
- if ($diff >= 1.001 && $diff < 12.001) {
- return '1M';
- }
-
- if ($diff >= 12.001) {
- return '1Y';
- }
-
- return '1D';
- }
-
- /**
- * If the date difference between start and end is less than a month, method returns "%Y-%m-%d". If the difference
- * is less than a year, method returns "%Y-%m". If the date difference is larger, method returns "%Y".
- */
- public function preferredSqlFormat(Carbon $start, Carbon $end): string
- {
- $diff = $start->diffInMonths($end, true);
- if ($diff >= 1.001 && $diff < 12.001) {
- return '%Y-%m';
- }
-
- if ($diff >= 12.001) {
- return '%Y';
- }
-
- return '%Y-%m-%d';
- }
-
/**
* @throws FireflyException
*/
@@ -680,7 +682,7 @@ class Navigation
/** @var Carbon $tEnd */
$tEnd = session('end', today(config('app.timezone'))->endOfMonth());
- $diffInDays = (int) $tStart->diffInDays($tEnd, true);
+ $diffInDays = (int)$tStart->diffInDays($tEnd, true);
$date->subDays($diffInDays * $subtract);
return $date;
diff --git a/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php b/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php
index 3f50036ab1..0c77493667 100644
--- a/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php
+++ b/app/Support/Observers/RecalculatesAvailableBudgetsTrait.php
@@ -39,6 +39,92 @@ use Spatie\Period\Precision;
trait RecalculatesAvailableBudgetsTrait
{
+ private function calculateAmount(AvailableBudget $availableBudget): void
+ {
+ $repository = app(BudgetLimitRepositoryInterface::class);
+ $repository->setUser($availableBudget->user);
+ $newAmount = '0';
+ $abPeriod = Period::make($availableBudget->start_date, $availableBudget->end_date, Precision::DAY());
+ Log::debug(
+ sprintf(
+ 'Now at AB #%d, ("%s" to "%s")',
+ $availableBudget->id,
+ $availableBudget->start_date->format('Y-m-d'),
+ $availableBudget->end_date->format('Y-m-d')
+ )
+ );
+ // have to recalculate everything just in case.
+ $set = $repository->getAllBudgetLimitsByCurrency($availableBudget->transactionCurrency, $availableBudget->start_date, $availableBudget->end_date);
+ Log::debug(sprintf('Found %d interesting budget limit(s).', $set->count()));
+
+ /** @var BudgetLimit $budgetLimit */
+ foreach ($set as $budgetLimit) {
+ Log::debug(
+ sprintf(
+ 'Found interesting budget limit #%d ("%s" to "%s")',
+ $budgetLimit->id,
+ $budgetLimit->start_date->format('Y-m-d'),
+ $budgetLimit->end_date->format('Y-m-d')
+ )
+ );
+ // overlap in days:
+ $limitPeriod = Period::make(
+ $budgetLimit->start_date,
+ $budgetLimit->end_date,
+ precision : Precision::DAY(),
+ boundaries: Boundaries::EXCLUDE_NONE()
+ );
+ // if both equal each other, amount from this BL must be added to the AB
+ if ($limitPeriod->equals($abPeriod)) {
+ Log::debug('This budget limit is equal to the available budget period.');
+ $newAmount = bcadd($newAmount, (string)$budgetLimit->amount);
+ }
+ // if budget limit period is inside AB period, it can be added in full.
+ if (!$limitPeriod->equals($abPeriod) && $abPeriod->contains($limitPeriod)) {
+ Log::debug('This budget limit is smaller than the available budget period.');
+ $newAmount = bcadd($newAmount, (string)$budgetLimit->amount);
+ }
+ if (!$limitPeriod->equals($abPeriod) && !$abPeriod->contains($limitPeriod) && $abPeriod->overlapsWith($limitPeriod)) {
+ Log::debug('This budget limit is something else entirely!');
+ $overlap = $abPeriod->overlap($limitPeriod);
+ if ($overlap instanceof Period) {
+ $length = $overlap->length();
+ $daily = bcmul($this->getDailyAmount($budgetLimit), (string)$length);
+ $newAmount = bcadd($newAmount, $daily);
+ }
+ }
+ }
+ if (0 === bccomp('0', $newAmount)) {
+ Log::debug('New amount is zero, deleting AB.');
+ $availableBudget->delete();
+
+ return;
+ }
+ Log::debug(sprintf('Concluded new amount for this AB must be %s', $newAmount));
+ $availableBudget->amount = app('steam')->bcround($newAmount, $availableBudget->transactionCurrency->decimal_places);
+ $availableBudget->save();
+ }
+
+ private function getDailyAmount(BudgetLimit $budgetLimit): string
+ {
+ if (0 === $budgetLimit->id) {
+ return '0';
+ }
+ $limitPeriod = Period::make(
+ $budgetLimit->start_date,
+ $budgetLimit->end_date,
+ precision : Precision::DAY(),
+ boundaries: Boundaries::EXCLUDE_NONE()
+ );
+ $days = $limitPeriod->length();
+ $amount = bcdiv($budgetLimit->amount, (string)$days, 12);
+ Log::debug(
+ sprintf('Total amount for budget limit #%d is %s. Nr. of days is %d. Amount per day is %s', $budgetLimit->id, $budgetLimit->amount, $days, $amount)
+ );
+
+ return $amount;
+ }
+
private function updateAvailableBudget(BudgetLimit $budgetLimit): void
{
Log::debug(sprintf('Now in updateAvailableBudget(limit #%d)', $budgetLimit->id));
@@ -83,7 +169,7 @@ trait RecalculatesAvailableBudgetsTrait
if (null === $viewRange || is_array($viewRange)) {
$viewRange = '1M';
}
- $viewRange = (string) $viewRange;
+ $viewRange = (string)$viewRange;
$start = app('navigation')->startOfPeriod($budgetLimit->start_date, $viewRange);
$end = app('navigation')->startOfPeriod($budgetLimit->end_date, $viewRange);
@@ -111,7 +197,7 @@ trait RecalculatesAvailableBudgetsTrait
// if not exists:
$currentPeriod = Period::make($current, $currentEnd, precision: Precision::DAY(), boundaries: Boundaries::EXCLUDE_NONE());
$daily = $this->getDailyAmount($budgetLimit);
- $amount = bcmul($daily, (string) $currentPeriod->length(), 12);
+ $amount = bcmul((string) $daily, (string)$currentPeriod->length(), 12);
// no need to calculate if period is equal.
if ($currentPeriod->equals($limitPeriod)) {
@@ -144,90 +230,4 @@ trait RecalculatesAvailableBudgetsTrait
$current = app('navigation')->addPeriod($current, $viewRange, 0);
}
}
-
- private function calculateAmount(AvailableBudget $availableBudget): void
- {
- $repository = app(BudgetLimitRepositoryInterface::class);
- $repository->setUser($availableBudget->user);
- $newAmount = '0';
- $abPeriod = Period::make($availableBudget->start_date, $availableBudget->end_date, Precision::DAY());
- Log::debug(
- sprintf(
- 'Now at AB #%d, ("%s" to "%s")',
- $availableBudget->id,
- $availableBudget->start_date->format('Y-m-d'),
- $availableBudget->end_date->format('Y-m-d')
- )
- );
- // have to recalculate everything just in case.
- $set = $repository->getAllBudgetLimitsByCurrency($availableBudget->transactionCurrency, $availableBudget->start_date, $availableBudget->end_date);
- Log::debug(sprintf('Found %d interesting budget limit(s).', $set->count()));
-
- /** @var BudgetLimit $budgetLimit */
- foreach ($set as $budgetLimit) {
- Log::debug(
- sprintf(
- 'Found interesting budget limit #%d ("%s" to "%s")',
- $budgetLimit->id,
- $budgetLimit->start_date->format('Y-m-d'),
- $budgetLimit->end_date->format('Y-m-d')
- )
- );
- // overlap in days:
- $limitPeriod = Period::make(
- $budgetLimit->start_date,
- $budgetLimit->end_date,
- precision : Precision::DAY(),
- boundaries: Boundaries::EXCLUDE_NONE()
- );
- // if both equal each other, amount from this BL must be added to the AB
- if ($limitPeriod->equals($abPeriod)) {
- Log::debug('This budget limit is equal to the available budget period.');
- $newAmount = bcadd($newAmount, (string) $budgetLimit->amount);
- }
- // if budget limit period is inside AB period, it can be added in full.
- if (!$limitPeriod->equals($abPeriod) && $abPeriod->contains($limitPeriod)) {
- Log::debug('This budget limit is smaller than the available budget period.');
- $newAmount = bcadd($newAmount, (string) $budgetLimit->amount);
- }
- if (!$limitPeriod->equals($abPeriod) && !$abPeriod->contains($limitPeriod) && $abPeriod->overlapsWith($limitPeriod)) {
- Log::debug('This budget limit is something else entirely!');
- $overlap = $abPeriod->overlap($limitPeriod);
- if ($overlap instanceof Period) {
- $length = $overlap->length();
- $daily = bcmul($this->getDailyAmount($budgetLimit), (string) $length);
- $newAmount = bcadd($newAmount, $daily);
- }
- }
- }
- if (0 === bccomp('0', $newAmount)) {
- Log::debug('New amount is zero, deleting AB.');
- $availableBudget->delete();
-
- return;
- }
- Log::debug(sprintf('Concluded new amount for this AB must be %s', $newAmount));
- $availableBudget->amount = app('steam')->bcround($newAmount, $availableBudget->transactionCurrency->decimal_places);
- $availableBudget->save();
- }
-
- private function getDailyAmount(BudgetLimit $budgetLimit): string
- {
- if (0 === $budgetLimit->id) {
- return '0';
- }
- $limitPeriod = Period::make(
- $budgetLimit->start_date,
- $budgetLimit->end_date,
- precision : Precision::DAY(),
- boundaries: Boundaries::EXCLUDE_NONE()
- );
- $days = $limitPeriod->length();
- $amount = bcdiv($budgetLimit->amount, (string) $days, 12);
- Log::debug(
- sprintf('Total amount for budget limit #%d is %s. Nr. of days is %d. Amount per day is %s', $budgetLimit->id, $budgetLimit->amount, $days, $amount)
- );
-
- return $amount;
- }
}
diff --git a/app/Support/ParseDateString.php b/app/Support/ParseDateString.php
index f2ab22a672..df60be3d1d 100644
--- a/app/Support/ParseDateString.php
+++ b/app/Support/ParseDateString.php
@@ -114,99 +114,13 @@ class ParseDateString
return new Carbon('1984-09-17');
}
// maybe a year, nothing else?
- if (4 === strlen($date) && is_numeric($date) && (int) $date > 1000 && (int) $date <= 3000) {
+ if (4 === strlen($date) && is_numeric($date) && (int)$date > 1000 && (int)$date <= 3000) {
return new Carbon(sprintf('%d-01-01', $date));
}
throw new FireflyException(sprintf('[d] Not a recognised date format: "%s"', $date));
}
- protected function parseKeyword(string $keyword): Carbon
- {
- $today = today(config('app.timezone'))->startOfDay();
-
- return match ($keyword) {
- default => $today,
- 'yesterday' => $today->subDay(),
- 'tomorrow' => $today->addDay(),
- 'start of this week' => $today->startOfWeek(CarbonInterface::MONDAY),
- 'end of this week' => $today->endOfWeek(CarbonInterface::SUNDAY),
- 'start of this month' => $today->startOfMonth(),
- 'end of this month' => $today->endOfMonth(),
- 'start of this quarter' => $today->startOfQuarter(),
- 'end of this quarter' => $today->endOfQuarter(),
- 'start of this year' => $today->startOfYear(),
- 'end of this year' => $today->endOfYear(),
- };
- }
-
- protected function parseDefaultDate(string $date): Carbon
- {
- $result = false;
-
- try {
- $result = Carbon::createFromFormat('Y-m-d', $date);
- } catch (InvalidFormatException $e) {
- Log::error(sprintf('parseDefaultDate("%s") ran into an error, but dont mind: %s', $date, $e->getMessage()));
- }
- if (false === $result) {
- return today(config('app.timezone'))->startOfDay();
- }
-
- return $result;
- }
-
- protected function parseRelativeDate(string $date): Carbon
- {
- Log::debug(sprintf('Now in parseRelativeDate("%s")', $date));
- $parts = explode(' ', $date);
- $today = today(config('app.timezone'))->startOfDay();
- $functions = [
- [
- 'd' => 'subDays',
- 'w' => 'subWeeks',
- 'm' => 'subMonths',
- 'q' => 'subQuarters',
- 'y' => 'subYears',
- ],
- [
- 'd' => 'addDays',
- 'w' => 'addWeeks',
- 'm' => 'addMonths',
- 'q' => 'addQuarters',
- 'y' => 'addYears',
- ],
- ];
-
- foreach ($parts as $part) {
- Log::debug(sprintf('Now parsing part "%s"', $part));
- $part = trim($part);
-
- // verify if correct
- $pattern = '/[+-]\d+[wqmdy]/';
- $result = preg_match($pattern, $part);
- if (0 === $result) {
- Log::error(sprintf('Part "%s" does not match regular expression. Will be skipped.', $part));
-
- continue;
- }
- $direction = str_starts_with($part, '+') ? 1 : 0;
- $period = $part[strlen($part) - 1];
- $number = (int) substr($part, 1, -1);
- if (!array_key_exists($period, $functions[$direction])) {
- Log::error(sprintf('No method for direction %d and period "%s".', $direction, $period));
-
- continue;
- }
- $func = $functions[$direction][$period];
- Log::debug(sprintf('Will now do %s(%d) on %s', $func, $number, $today->format('Y-m-d')));
- $today->{$func}($number); // @phpstan-ignore-line
- Log::debug(sprintf('Resulting date is %s', $today->format('Y-m-d')));
- }
-
- return $today;
- }
-
public function parseRange(string $date): array
{
// several types of range can be submitted
@@ -269,16 +183,34 @@ class ParseDateString
return false;
}
- /**
- * format of string is xxxx-xx-DD
- */
- protected function parseDayRange(string $date): array
+ protected function isDayYearRange(string $date): bool
{
- $parts = explode('-', $date);
+ // if regex for YYYY-xx-DD:
+ $pattern = '/^(19|20)\d\d-xx-(0[1-9]|[12]\d|3[01])$/';
+ $result = preg_match($pattern, $date);
+ if (0 !== $result) {
+ Log::debug(sprintf('"%s" is a day/year range.', $date));
- return [
- 'day' => $parts[2],
- ];
+ return true;
+ }
+ Log::debug(sprintf('"%s" is not a day/year range.', $date));
+
+ return false;
+ }
+
+ protected function isMonthDayRange(string $date): bool
+ {
+ // if regex for xxxx-MM-DD:
+ $pattern = '/^xxxx-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])$/';
+ $result = preg_match($pattern, $date);
+ if (0 !== $result) {
+ Log::debug(sprintf('"%s" is a month/day range.', $date));
+
+ return true;
+ }
+ Log::debug(sprintf('"%s" is not a month/day range.', $date));
+
+ return false;
}
protected function isMonthRange(string $date): bool
@@ -296,17 +228,19 @@ class ParseDateString
return false;
}
- /**
- * format of string is xxxx-MM-xx
- */
- protected function parseMonthRange(string $date): array
+ protected function isMonthYearRange(string $date): bool
{
- Log::debug(sprintf('parseMonthRange: Parsed "%s".', $date));
- $parts = explode('-', $date);
+ // if regex for YYYY-MM-xx:
+ $pattern = '/^(19|20)\d\d-(0[1-9]|1[012])-xx$/';
+ $result = preg_match($pattern, $date);
+ if (0 !== $result) {
+ Log::debug(sprintf('"%s" is a month/year range.', $date));
- return [
- 'month' => $parts[1],
- ];
+ return true;
+ }
+ Log::debug(sprintf('"%s" is not a month/year range.', $date));
+
+ return false;
}
protected function isYearRange(string $date): bool
@@ -324,6 +258,131 @@ class ParseDateString
return false;
}
+ /**
+ * format of string is xxxx-xx-DD
+ */
+ protected function parseDayRange(string $date): array
+ {
+ $parts = explode('-', $date);
+
+ return [
+ 'day' => $parts[2],
+ ];
+ }
+
+ protected function parseDefaultDate(string $date): Carbon
+ {
+ $result = false;
+
+ try {
+ $result = Carbon::createFromFormat('Y-m-d', $date);
+ } catch (InvalidFormatException $e) {
+ Log::error(sprintf('parseDefaultDate("%s") ran into an error, but dont mind: %s', $date, $e->getMessage()));
+ }
+ if (false === $result) {
+ return today(config('app.timezone'))->startOfDay();
+ }
+
+ return $result;
+ }
+
+ protected function parseKeyword(string $keyword): Carbon
+ {
+ $today = today(config('app.timezone'))->startOfDay();
+
+ return match ($keyword) {
+ default => $today,
+ 'yesterday' => $today->subDay(),
+ 'tomorrow' => $today->addDay(),
+ 'start of this week' => $today->startOfWeek(CarbonInterface::MONDAY),
+ 'end of this week' => $today->endOfWeek(CarbonInterface::SUNDAY),
+ 'start of this month' => $today->startOfMonth(),
+ 'end of this month' => $today->endOfMonth(),
+ 'start of this quarter' => $today->startOfQuarter(),
+ 'end of this quarter' => $today->endOfQuarter(),
+ 'start of this year' => $today->startOfYear(),
+ 'end of this year' => $today->endOfYear(),
+ };
+ }
+
+ /**
+ * format of string is xxxx-MM-xx
+ */
+ protected function parseMonthRange(string $date): array
+ {
+ Log::debug(sprintf('parseMonthRange: Parsed "%s".', $date));
+ $parts = explode('-', $date);
+
+ return [
+ 'month' => $parts[1],
+ ];
+ }
+
+ /**
+ * format of string is YYYY-MM-xx
+ */
+ protected function parseMonthYearRange(string $date): array
+ {
+ Log::debug(sprintf('parseMonthYearRange: Parsed "%s".', $date));
+ $parts = explode('-', $date);
+
+ return [
+ 'year' => $parts[0],
+ 'month' => $parts[1],
+ ];
+ }
+
+ protected function parseRelativeDate(string $date): Carbon
+ {
+ Log::debug(sprintf('Now in parseRelativeDate("%s")', $date));
+ $parts = explode(' ', $date);
+ $today = today(config('app.timezone'))->startOfDay();
+ $functions = [
+ [
+ 'd' => 'subDays',
+ 'w' => 'subWeeks',
+ 'm' => 'subMonths',
+ 'q' => 'subQuarters',
+ 'y' => 'subYears',
+ ],
+ [
+ 'd' => 'addDays',
+ 'w' => 'addWeeks',
+ 'm' => 'addMonths',
+ 'q' => 'addQuarters',
+ 'y' => 'addYears',
+ ],
+ ];
+
+ foreach ($parts as $part) {
+ Log::debug(sprintf('Now parsing part "%s"', $part));
+ $part = trim($part);
+
+ // verify if correct
+ $pattern = '/[+-]\d+[wqmdy]/';
+ $result = preg_match($pattern, $part);
+ if (0 === $result) {
+ Log::error(sprintf('Part "%s" does not match regular expression. Will be skipped.', $part));
+
+ continue;
+ }
+ $direction = str_starts_with($part, '+') ? 1 : 0;
+ $period = $part[strlen($part) - 1];
+ $number = (int)substr($part, 1, -1);
+ if (!array_key_exists($period, $functions[$direction])) {
+ Log::error(sprintf('No method for direction %d and period "%s".', $direction, $period));
+
+ continue;
+ }
+ $func = $functions[$direction][$period];
+ Log::debug(sprintf('Will now do %s(%d) on %s', $func, $number, $today->format('Y-m-d')));
+ $today->{$func}($number); // @phpstan-ignore-line
+ Log::debug(sprintf('Resulting date is %s', $today->format('Y-m-d')));
+ }
+
+ return $today;
+ }
+
/**
* format of string is YYYY-xx-xx
*/
@@ -337,50 +396,6 @@ class ParseDateString
];
}
- protected function isMonthDayRange(string $date): bool
- {
- // if regex for xxxx-MM-DD:
- $pattern = '/^xxxx-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])$/';
- $result = preg_match($pattern, $date);
- if (0 !== $result) {
- Log::debug(sprintf('"%s" is a month/day range.', $date));
-
- return true;
- }
- Log::debug(sprintf('"%s" is not a month/day range.', $date));
-
- return false;
- }
-
- /**
- * format of string is xxxx-MM-DD
- */
- private function parseMonthDayRange(string $date): array
- {
- Log::debug(sprintf('parseMonthDayRange: Parsed "%s".', $date));
- $parts = explode('-', $date);
-
- return [
- 'month' => $parts[1],
- 'day' => $parts[2],
- ];
- }
-
- protected function isDayYearRange(string $date): bool
- {
- // if regex for YYYY-xx-DD:
- $pattern = '/^(19|20)\d\d-xx-(0[1-9]|[12]\d|3[01])$/';
- $result = preg_match($pattern, $date);
- if (0 !== $result) {
- Log::debug(sprintf('"%s" is a day/year range.', $date));
-
- return true;
- }
- Log::debug(sprintf('"%s" is not a day/year range.', $date));
-
- return false;
- }
-
/**
* format of string is YYYY-xx-DD
*/
@@ -395,32 +410,17 @@ class ParseDateString
];
}
- protected function isMonthYearRange(string $date): bool
- {
- // if regex for YYYY-MM-xx:
- $pattern = '/^(19|20)\d\d-(0[1-9]|1[012])-xx$/';
- $result = preg_match($pattern, $date);
- if (0 !== $result) {
- Log::debug(sprintf('"%s" is a month/year range.', $date));
-
- return true;
- }
- Log::debug(sprintf('"%s" is not a month/year range.', $date));
-
- return false;
- }
-
/**
- * format of string is YYYY-MM-xx
+ * format of string is xxxx-MM-DD
*/
- protected function parseMonthYearRange(string $date): array
+ private function parseMonthDayRange(string $date): array
{
- Log::debug(sprintf('parseMonthYearRange: Parsed "%s".', $date));
+ Log::debug(sprintf('parseMonthDayRange: Parsed "%s".', $date));
$parts = explode('-', $date);
return [
- 'year' => $parts[0],
'month' => $parts[1],
+ 'day' => $parts[2],
];
}
}
diff --git a/app/Support/Preferences.php b/app/Support/Preferences.php
index 4a068cae06..d622f1f66f 100644
--- a/app/Support/Preferences.php
+++ b/app/Support/Preferences.php
@@ -57,64 +57,11 @@ class Preferences
;
}
- public function get(string $name, array|bool|int|string|null $default = null): ?Preference
+ public function beginsWith(User $user, string $search): Collection
{
- /** @var null|User $user */
- $user = auth()->user();
- if (null === $user) {
- $preference = new Preference();
- $preference->data = $default;
+ $value = sprintf('%s%%', $search);
- return $preference;
- }
-
- return $this->getForUser($user, $name, $default);
- }
-
- public function getForUser(User $user, string $name, array|bool|int|string|null $default = null): ?Preference
- {
- // Log::debug(sprintf('getForUser(#%d, "%s")', $user->id, $name));
- // don't care about user group ID, except for some specific preferences.
- $userGroupId = $this->getUserGroupId($user, $name);
- $query = Preference::where('user_id', $user->id)->where('name', $name);
- if (null !== $userGroupId) {
- Log::debug('Include user group ID in query');
- $query->where('user_group_id', $userGroupId);
- }
-
- $preference = $query->first(['id', 'user_id', 'user_group_id', 'name', 'data', 'updated_at', 'created_at']);
-
- if (null !== $preference && null === $preference->data) {
- $preference->delete();
- $preference = null;
- Log::debug('Removed empty preference.');
- }
-
- if (null !== $preference) {
- // Log::debug(sprintf('Found preference #%d for user #%d: %s', $preference->id, $user->id, $name));
-
- return $preference;
- }
- // no preference found and default is null:
- if (null === $default) {
- Log::debug('Return NULL, create no preference.');
-
- // return NULL
- return null;
- }
-
- return $this->setForUser($user, $name, $default);
- }
-
- private function getUserGroupId(User $user, string $preferenceName): ?int
- {
- $groupId = null;
- $items = config('firefly.admin_specific_prefs') ?? [];
- if (in_array($preferenceName, $items, true)) {
- return (int) $user->user_group_id;
- }
-
- return $groupId;
+ return Preference::where('user_id', $user->id)->whereLike('name', $value)->get();
}
public function delete(string $name): bool
@@ -128,6 +75,14 @@ class Preferences
return true;
}
+ /**
+ * Find by name, has no user ID in it, because the method is called from an unauthenticated route any way.
+ */
+ public function findByName(string $name): Collection
+ {
+ return Preference::where('name', $name)->get();
+ }
+
public function forget(User $user, string $name): void
{
$key = sprintf('preference%s%s', $user->id, $name);
@@ -135,57 +90,18 @@ class Preferences
Cache::put($key, '', 5);
}
- public function setForUser(User $user, string $name, array|bool|int|string|null $value): Preference
+ public function get(string $name, array|bool|int|string|null $default = null): ?Preference
{
- $fullName = sprintf('preference%s%s', $user->id, $name);
- $userGroupId = $this->getUserGroupId($user, $name);
- $userGroupId = 0 === (int) $userGroupId ? null : (int) $userGroupId;
+ /** @var null|User $user */
+ $user = auth()->user();
+ if (null === $user) {
+ $preference = new Preference();
+ $preference->data = $default;
- Cache::forget($fullName);
-
- $query = Preference::where('user_id', $user->id)->where('name', $name);
- if (null !== $userGroupId) {
- Log::debug('Include user group ID in query');
- $query->where('user_group_id', $userGroupId);
+ return $preference;
}
- $preference = $query->first(['id', 'user_id', 'user_group_id', 'name', 'data', 'updated_at', 'created_at']);
-
- if (null !== $preference && null === $value) {
- $preference->delete();
-
- return new Preference();
- }
- if (null === $value) {
- return new Preference();
- }
- if (null === $preference) {
- $preference = new Preference();
- $preference->user_id = (int) $user->id;
- $preference->user_group_id = $userGroupId;
- $preference->name = $name;
-
- }
- $preference->data = $value;
- $preference->save();
- Cache::forever($fullName, $preference);
-
- return $preference;
- }
-
- public function beginsWith(User $user, string $search): Collection
- {
- $value = sprintf('%s%%', $search);
-
- return Preference::where('user_id', $user->id)->whereLike('name', $value)->get();
- }
-
- /**
- * Find by name, has no user ID in it, because the method is called from an unauthenticated route any way.
- */
- public function findByName(string $name): Collection
- {
- return Preference::where('name', $name)->get();
+ return $this->getForUser($user, $name, $default);
}
public function getArrayForUser(User $user, array $list): array
@@ -265,6 +181,41 @@ class Preferences
return $result;
}
+ public function getForUser(User $user, string $name, array|bool|int|string|null $default = null): ?Preference
+ {
+ // Log::debug(sprintf('getForUser(#%d, "%s")', $user->id, $name));
+ // don't care about user group ID, except for some specific preferences.
+ $userGroupId = $this->getUserGroupId($user, $name);
+ $query = Preference::where('user_id', $user->id)->where('name', $name);
+ if (null !== $userGroupId) {
+ Log::debug('Include user group ID in query');
+ $query->where('user_group_id', $userGroupId);
+ }
+
+ $preference = $query->first(['id', 'user_id', 'user_group_id', 'name', 'data', 'updated_at', 'created_at']);
+
+ if (null !== $preference && null === $preference->data) {
+ $preference->delete();
+ $preference = null;
+ Log::debug('Removed empty preference.');
+ }
+
+ if (null !== $preference) {
+ // Log::debug(sprintf('Found preference #%d for user #%d: %s', $preference->id, $user->id, $name));
+
+ return $preference;
+ }
+ // no preference found and default is null:
+ if (null === $default) {
+ Log::debug('Return NULL, create no preference.');
+
+ // return NULL
+ return null;
+ }
+
+ return $this->setForUser($user, $name, $default);
+ }
+
public function getFresh(string $name, array|bool|int|string|null $default = null): ?Preference
{
/** @var null|User $user */
@@ -299,7 +250,7 @@ class Preferences
if (is_array($lastActivity)) {
$lastActivity = implode(',', $lastActivity);
}
- $setting = hash('sha256', (string) $lastActivity);
+ $setting = hash('sha256', (string)$lastActivity);
$instance->setPreference('last_activity', $setting);
return $setting;
@@ -341,4 +292,53 @@ class Preferences
return $this->set($name, $encrypted);
}
+
+ public function setForUser(User $user, string $name, array|bool|int|string|null $value): Preference
+ {
+ $fullName = sprintf('preference%s%s', $user->id, $name);
+ $userGroupId = $this->getUserGroupId($user, $name);
+ $userGroupId = 0 === (int)$userGroupId ? null : (int)$userGroupId;
+
+ Cache::forget($fullName);
+
+ $query = Preference::where('user_id', $user->id)->where('name', $name);
+ if (null !== $userGroupId) {
+ Log::debug('Include user group ID in query');
+ $query->where('user_group_id', $userGroupId);
+ }
+
+ $preference = $query->first(['id', 'user_id', 'user_group_id', 'name', 'data', 'updated_at', 'created_at']);
+
+ if (null !== $preference && null === $value) {
+ $preference->delete();
+
+ return new Preference();
+ }
+ if (null === $value) {
+ return new Preference();
+ }
+ if (null === $preference) {
+ $preference = new Preference();
+ $preference->user_id = (int)$user->id;
+ $preference->user_group_id = $userGroupId;
+ $preference->name = $name;
+
+ }
+ $preference->data = $value;
+ $preference->save();
+ Cache::forever($fullName, $preference);
+
+ return $preference;
+ }
+
+ private function getUserGroupId(User $user, string $preferenceName): ?int
+ {
+ $groupId = null;
+ $items = config('firefly.admin_specific_prefs') ?? [];
+ if (in_array($preferenceName, $items, true)) {
+ return (int)$user->user_group_id;
+ }
+
+ return $groupId;
+ }
}
diff --git a/app/Support/Report/Budget/BudgetReportGenerator.php b/app/Support/Report/Budget/BudgetReportGenerator.php
index c478e3cce4..4de1c9d9a6 100644
--- a/app/Support/Report/Budget/BudgetReportGenerator.php
+++ b/app/Support/Report/Budget/BudgetReportGenerator.php
@@ -91,43 +91,6 @@ class BudgetReportGenerator
}
}
- /**
- * Process each row of expenses collected for the "Account per budget" partial
- */
- private function processExpenses(array $expenses): void
- {
- foreach ($expenses['budgets'] as $budget) {
- $this->processBudgetExpenses($expenses, $budget);
- }
- }
-
- /**
- * Process each set of transactions for each row of expenses.
- */
- private function processBudgetExpenses(array $expenses, array $budget): void
- {
- $budgetId = (int) $budget['id'];
- $currencyId = (int) $expenses['currency_id'];
- foreach ($budget['transaction_journals'] as $journal) {
- $sourceAccountId = $journal['source_account_id'];
-
- $this->report[$sourceAccountId]['currencies'][$currencyId]
- ??= [
- 'currency_id' => $expenses['currency_id'],
- 'currency_symbol' => $expenses['currency_symbol'],
- 'currency_name' => $expenses['currency_name'],
- 'currency_decimal_places' => $expenses['currency_decimal_places'],
- 'budgets' => [],
- ];
-
- $this->report[$sourceAccountId]['currencies'][$currencyId]['budgets'][$budgetId]
- ??= '0';
-
- $this->report[$sourceAccountId]['currencies'][$currencyId]['budgets'][$budgetId]
- = bcadd($this->report[$sourceAccountId]['currencies'][$currencyId]['budgets'][$budgetId], (string) $journal['amount']);
- }
- }
-
/**
* Generates the data necessary to create the card that displays
* the budget overview in the general report.
@@ -144,6 +107,43 @@ class BudgetReportGenerator
$this->percentageReport();
}
+ public function getReport(): array
+ {
+ return $this->report;
+ }
+
+ public function setAccounts(Collection $accounts): void
+ {
+ $this->accounts = $accounts;
+ }
+
+ public function setBudgets(Collection $budgets): void
+ {
+ $this->budgets = $budgets;
+ }
+
+ public function setEnd(Carbon $end): void
+ {
+ $this->end = $end;
+ }
+
+ public function setStart(Carbon $start): void
+ {
+ $this->start = $start;
+ }
+
+ /**
+ * @throws FireflyException
+ */
+ public function setUser(User $user): void
+ {
+ $this->repository->setUser($user);
+ $this->blRepository->setUser($user);
+ $this->opsRepository->setUser($user);
+ $this->nbRepository->setUser($user);
+ $this->currency = app('amount')->getPrimaryCurrencyByUserGroup($user->userGroup);
+ }
+
/**
* Start the budgets block on the default report by processing every budget.
*/
@@ -157,6 +157,90 @@ class BudgetReportGenerator
}
}
+ /**
+ * Calculate the expenses for transactions without a budget. Part of the "budgets" block of the default report.
+ */
+ private function noBudgetReport(): void
+ {
+ // add no budget info.
+ $this->report['budgets'][0] = [
+ 'budget_id' => null,
+ 'budget_name' => null,
+ 'no_budget' => true,
+ 'budget_limits' => [],
+ ];
+
+ $noBudget = $this->nbRepository->sumExpenses($this->start, $this->end, $this->accounts);
+ foreach ($noBudget as $noBudgetEntry) {
+ // currency information:
+ $nbCurrencyId = (int)($noBudgetEntry['currency_id'] ?? $this->currency->id);
+ $nbCurrencyCode = $noBudgetEntry['currency_code'] ?? $this->currency->code;
+ $nbCurrencyName = $noBudgetEntry['currency_name'] ?? $this->currency->name;
+ $nbCurrencySymbol = $noBudgetEntry['currency_symbol'] ?? $this->currency->symbol;
+ $nbCurrencyDp = $noBudgetEntry['currency_decimal_places'] ?? $this->currency->decimal_places;
+
+ $this->report['budgets'][0]['budget_limits'][] = [
+ 'budget_limit_id' => null,
+ 'start_date' => $this->start,
+ 'end_date' => $this->end,
+ 'budgeted' => '0',
+ 'budgeted_pct' => '0',
+ 'spent' => $noBudgetEntry['sum'],
+ 'spent_pct' => '0',
+ 'left' => '0',
+ 'overspent' => '0',
+ 'currency_id' => $nbCurrencyId,
+ 'currency_code' => $nbCurrencyCode,
+ 'currency_name' => $nbCurrencyName,
+ 'currency_symbol' => $nbCurrencySymbol,
+ 'currency_decimal_places' => $nbCurrencyDp,
+ ];
+ $this->report['sums'][$nbCurrencyId]['spent'] = bcadd($this->report['sums'][$nbCurrencyId]['spent'] ?? '0', (string)$noBudgetEntry['sum']);
+ // append currency info because it may be missing:
+ $this->report['sums'][$nbCurrencyId]['currency_id'] = $nbCurrencyId;
+ $this->report['sums'][$nbCurrencyId]['currency_code'] = $nbCurrencyCode;
+ $this->report['sums'][$nbCurrencyId]['currency_name'] = $nbCurrencyName;
+ $this->report['sums'][$nbCurrencyId]['currency_symbol'] = $nbCurrencySymbol;
+ $this->report['sums'][$nbCurrencyId]['currency_decimal_places'] = $nbCurrencyDp;
+
+ // append other sums because they might be missing:
+ $this->report['sums'][$nbCurrencyId]['overspent'] ??= '0';
+ $this->report['sums'][$nbCurrencyId]['left'] ??= '0';
+ $this->report['sums'][$nbCurrencyId]['budgeted'] ??= '0';
+ }
+ }
+
+ /**
+ * Calculate the percentages for each budget. Part of the "budgets" block on the default report.
+ */
+ private function percentageReport(): void
+ {
+ // make percentages based on total amount.
+ foreach ($this->report['budgets'] as $budgetId => $data) {
+ foreach ($data['budget_limits'] as $limitId => $entry) {
+ $budgetId = (int)$budgetId;
+ $limitId = (int)$limitId;
+ $currencyId = (int)$entry['currency_id'];
+ $spent = $entry['spent'];
+ $totalSpent = $this->report['sums'][$currencyId]['spent'] ?? '0';
+ $spentPct = '0';
+ $budgeted = $entry['budgeted'];
+ $totalBudgeted = $this->report['sums'][$currencyId]['budgeted'] ?? '0';
+ $budgetedPct = '0';
+
+ if (0 !== bccomp((string)$spent, '0') && 0 !== bccomp($totalSpent, '0')) {
+ $spentPct = round((float)bcmul(bcdiv((string)$spent, $totalSpent), '100'));
+ }
+ if (0 !== bccomp((string)$budgeted, '0') && 0 !== bccomp($totalBudgeted, '0')) {
+ $budgetedPct = round((float)bcmul(bcdiv((string)$budgeted, $totalBudgeted), '100'));
+ }
+ $this->report['sums'][$currencyId]['budgeted'] ??= '0';
+ $this->report['budgets'][$budgetId]['budget_limits'][$limitId]['spent_pct'] = $spentPct;
+ $this->report['budgets'][$budgetId]['budget_limits'][$limitId]['budgeted_pct'] = $budgetedPct;
+ }
+ }
+ }
+
/**
* Process expenses etc. for a single budget for the budgets block on the default report.
*/
@@ -179,6 +263,43 @@ class BudgetReportGenerator
}
}
+ /**
+ * Process each set of transactions for each row of expenses.
+ */
+ private function processBudgetExpenses(array $expenses, array $budget): void
+ {
+ $budgetId = (int)$budget['id'];
+ $currencyId = (int)$expenses['currency_id'];
+ foreach ($budget['transaction_journals'] as $journal) {
+ $sourceAccountId = $journal['source_account_id'];
+
+ $this->report[$sourceAccountId]['currencies'][$currencyId]
+ ??= [
+ 'currency_id' => $expenses['currency_id'],
+ 'currency_symbol' => $expenses['currency_symbol'],
+ 'currency_name' => $expenses['currency_name'],
+ 'currency_decimal_places' => $expenses['currency_decimal_places'],
+ 'budgets' => [],
+ ];
+
+ $this->report[$sourceAccountId]['currencies'][$currencyId]['budgets'][$budgetId]
+ ??= '0';
+
+ $this->report[$sourceAccountId]['currencies'][$currencyId]['budgets'][$budgetId]
+ = bcadd($this->report[$sourceAccountId]['currencies'][$currencyId]['budgets'][$budgetId], (string)$journal['amount']);
+ }
+ }
+
+ /**
+ * Process each row of expenses collected for the "Account per budget" partial
+ */
+ private function processExpenses(array $expenses): void
+ {
+ foreach ($expenses['budgets'] as $budget) {
+ $this->processBudgetExpenses($expenses, $budget);
+ }
+ }
+
/**
* Process a single budget limit for the budgets block on the default report.
*/
@@ -223,130 +344,9 @@ class BudgetReportGenerator
'currency_symbol' => $limitCurrency->symbol,
'currency_decimal_places' => $limitCurrency->decimal_places,
];
- $this->report['sums'][$currencyId]['budgeted'] = bcadd((string) $this->report['sums'][$currencyId]['budgeted'], $limit->amount);
- $this->report['sums'][$currencyId]['spent'] = bcadd((string) $this->report['sums'][$currencyId]['spent'], $spent);
- $this->report['sums'][$currencyId]['left'] = bcadd((string) $this->report['sums'][$currencyId]['left'], bcadd($limit->amount, $spent));
- $this->report['sums'][$currencyId]['overspent'] = bcadd((string) $this->report['sums'][$currencyId]['overspent'], $overspent);
- }
-
- /**
- * Calculate the expenses for transactions without a budget. Part of the "budgets" block of the default report.
- */
- private function noBudgetReport(): void
- {
- // add no budget info.
- $this->report['budgets'][0] = [
- 'budget_id' => null,
- 'budget_name' => null,
- 'no_budget' => true,
- 'budget_limits' => [],
- ];
-
- $noBudget = $this->nbRepository->sumExpenses($this->start, $this->end, $this->accounts);
- foreach ($noBudget as $noBudgetEntry) {
- // currency information:
- $nbCurrencyId = (int) ($noBudgetEntry['currency_id'] ?? $this->currency->id);
- $nbCurrencyCode = $noBudgetEntry['currency_code'] ?? $this->currency->code;
- $nbCurrencyName = $noBudgetEntry['currency_name'] ?? $this->currency->name;
- $nbCurrencySymbol = $noBudgetEntry['currency_symbol'] ?? $this->currency->symbol;
- $nbCurrencyDp = $noBudgetEntry['currency_decimal_places'] ?? $this->currency->decimal_places;
-
- $this->report['budgets'][0]['budget_limits'][] = [
- 'budget_limit_id' => null,
- 'start_date' => $this->start,
- 'end_date' => $this->end,
- 'budgeted' => '0',
- 'budgeted_pct' => '0',
- 'spent' => $noBudgetEntry['sum'],
- 'spent_pct' => '0',
- 'left' => '0',
- 'overspent' => '0',
- 'currency_id' => $nbCurrencyId,
- 'currency_code' => $nbCurrencyCode,
- 'currency_name' => $nbCurrencyName,
- 'currency_symbol' => $nbCurrencySymbol,
- 'currency_decimal_places' => $nbCurrencyDp,
- ];
- $this->report['sums'][$nbCurrencyId]['spent'] = bcadd($this->report['sums'][$nbCurrencyId]['spent'] ?? '0', (string) $noBudgetEntry['sum']);
- // append currency info because it may be missing:
- $this->report['sums'][$nbCurrencyId]['currency_id'] = $nbCurrencyId;
- $this->report['sums'][$nbCurrencyId]['currency_code'] = $nbCurrencyCode;
- $this->report['sums'][$nbCurrencyId]['currency_name'] = $nbCurrencyName;
- $this->report['sums'][$nbCurrencyId]['currency_symbol'] = $nbCurrencySymbol;
- $this->report['sums'][$nbCurrencyId]['currency_decimal_places'] = $nbCurrencyDp;
-
- // append other sums because they might be missing:
- $this->report['sums'][$nbCurrencyId]['overspent'] ??= '0';
- $this->report['sums'][$nbCurrencyId]['left'] ??= '0';
- $this->report['sums'][$nbCurrencyId]['budgeted'] ??= '0';
- }
- }
-
- /**
- * Calculate the percentages for each budget. Part of the "budgets" block on the default report.
- */
- private function percentageReport(): void
- {
- // make percentages based on total amount.
- foreach ($this->report['budgets'] as $budgetId => $data) {
- foreach ($data['budget_limits'] as $limitId => $entry) {
- $budgetId = (int) $budgetId;
- $limitId = (int) $limitId;
- $currencyId = (int) $entry['currency_id'];
- $spent = $entry['spent'];
- $totalSpent = $this->report['sums'][$currencyId]['spent'] ?? '0';
- $spentPct = '0';
- $budgeted = $entry['budgeted'];
- $totalBudgeted = $this->report['sums'][$currencyId]['budgeted'] ?? '0';
- $budgetedPct = '0';
-
- if (0 !== bccomp((string) $spent, '0') && 0 !== bccomp($totalSpent, '0')) {
- $spentPct = round((float) bcmul(bcdiv((string) $spent, $totalSpent), '100'));
- }
- if (0 !== bccomp((string) $budgeted, '0') && 0 !== bccomp($totalBudgeted, '0')) {
- $budgetedPct = round((float) bcmul(bcdiv((string) $budgeted, $totalBudgeted), '100'));
- }
- $this->report['sums'][$currencyId]['budgeted'] ??= '0';
- $this->report['budgets'][$budgetId]['budget_limits'][$limitId]['spent_pct'] = $spentPct;
- $this->report['budgets'][$budgetId]['budget_limits'][$limitId]['budgeted_pct'] = $budgetedPct;
- }
- }
- }
-
- public function getReport(): array
- {
- return $this->report;
- }
-
- public function setAccounts(Collection $accounts): void
- {
- $this->accounts = $accounts;
- }
-
- public function setBudgets(Collection $budgets): void
- {
- $this->budgets = $budgets;
- }
-
- public function setEnd(Carbon $end): void
- {
- $this->end = $end;
- }
-
- public function setStart(Carbon $start): void
- {
- $this->start = $start;
- }
-
- /**
- * @throws FireflyException
- */
- public function setUser(User $user): void
- {
- $this->repository->setUser($user);
- $this->blRepository->setUser($user);
- $this->opsRepository->setUser($user);
- $this->nbRepository->setUser($user);
- $this->currency = app('amount')->getPrimaryCurrencyByUserGroup($user->userGroup);
+ $this->report['sums'][$currencyId]['budgeted'] = bcadd((string)$this->report['sums'][$currencyId]['budgeted'], $limit->amount);
+ $this->report['sums'][$currencyId]['spent'] = bcadd((string)$this->report['sums'][$currencyId]['spent'], $spent);
+ $this->report['sums'][$currencyId]['left'] = bcadd((string)$this->report['sums'][$currencyId]['left'], bcadd($limit->amount, $spent));
+ $this->report['sums'][$currencyId]['overspent'] = bcadd((string)$this->report['sums'][$currencyId]['overspent'], $overspent);
}
}
diff --git a/app/Support/Report/Category/CategoryReportGenerator.php b/app/Support/Report/Category/CategoryReportGenerator.php
index 91f1470bb8..51570e7696 100644
--- a/app/Support/Report/Category/CategoryReportGenerator.php
+++ b/app/Support/Report/Category/CategoryReportGenerator.php
@@ -83,17 +83,69 @@ class CategoryReportGenerator
}
}
- /**
- * Process one of the spent arrays from the operations method.
- */
- private function processOpsArray(array $data): void
+ public function setAccounts(Collection $accounts): void
{
- /**
- * @var int $currencyId
- * @var array $currencyRow
- */
- foreach ($data as $currencyId => $currencyRow) {
- $this->processCurrencyArray($currencyId, $currencyRow);
+ $this->accounts = $accounts;
+ }
+
+ public function setEnd(Carbon $end): void
+ {
+ $this->end = $end;
+ }
+
+ public function setStart(Carbon $start): void
+ {
+ $this->start = $start;
+ }
+
+ public function setUser(User $user): void
+ {
+ $this->noCatRepository->setUser($user);
+ $this->opsRepository->setUser($user);
+ }
+
+ private function processCategoryRow(int $currencyId, array $currencyRow, int $categoryId, array $categoryRow): void
+ {
+ $key = sprintf('%s-%s', $currencyId, $categoryId);
+ $this->report['categories'][$key] ??= [
+ 'id' => $categoryId,
+ 'title' => $categoryRow['name'],
+ 'currency_id' => $currencyRow['currency_id'],
+ 'currency_symbol' => $currencyRow['currency_symbol'],
+ 'currency_name' => $currencyRow['currency_name'],
+ 'currency_code' => $currencyRow['currency_code'],
+ 'currency_decimal_places' => $currencyRow['currency_decimal_places'],
+ 'spent' => '0',
+ 'earned' => '0',
+ 'sum' => '0',
+ ];
+ // loop journals:
+ foreach ($categoryRow['transaction_journals'] as $journal) {
+ // sum of sums
+ $this->report['sums'][$currencyId]['sum'] = bcadd((string)$this->report['sums'][$currencyId]['sum'], (string)$journal['amount']);
+ // sum of spent:
+ $this->report['sums'][$currencyId]['spent'] = -1 === bccomp((string)$journal['amount'], '0') ? bcadd(
+ (string)$this->report['sums'][$currencyId]['spent'],
+ (string)$journal['amount']
+ ) : $this->report['sums'][$currencyId]['spent'];
+ // sum of earned
+ $this->report['sums'][$currencyId]['earned'] = 1 === bccomp((string)$journal['amount'], '0') ? bcadd(
+ (string)$this->report['sums'][$currencyId]['earned'],
+ (string)$journal['amount']
+ ) : $this->report['sums'][$currencyId]['earned'];
+
+ // sum of category
+ $this->report['categories'][$key]['sum'] = bcadd((string)$this->report['categories'][$key]['sum'], (string)$journal['amount']);
+ // total spent in category
+ $this->report['categories'][$key]['spent'] = -1 === bccomp((string)$journal['amount'], '0') ? bcadd(
+ (string)$this->report['categories'][$key]['spent'],
+ (string)$journal['amount']
+ ) : $this->report['categories'][$key]['spent'];
+ // total earned in category
+ $this->report['categories'][$key]['earned'] = 1 === bccomp((string)$journal['amount'], '0') ? bcadd(
+ (string)$this->report['categories'][$key]['earned'],
+ (string)$journal['amount']
+ ) : $this->report['categories'][$key]['earned'];
}
}
@@ -119,69 +171,17 @@ class CategoryReportGenerator
}
}
- private function processCategoryRow(int $currencyId, array $currencyRow, int $categoryId, array $categoryRow): void
+ /**
+ * Process one of the spent arrays from the operations method.
+ */
+ private function processOpsArray(array $data): void
{
- $key = sprintf('%s-%s', $currencyId, $categoryId);
- $this->report['categories'][$key] ??= [
- 'id' => $categoryId,
- 'title' => $categoryRow['name'],
- 'currency_id' => $currencyRow['currency_id'],
- 'currency_symbol' => $currencyRow['currency_symbol'],
- 'currency_name' => $currencyRow['currency_name'],
- 'currency_code' => $currencyRow['currency_code'],
- 'currency_decimal_places' => $currencyRow['currency_decimal_places'],
- 'spent' => '0',
- 'earned' => '0',
- 'sum' => '0',
- ];
- // loop journals:
- foreach ($categoryRow['transaction_journals'] as $journal) {
- // sum of sums
- $this->report['sums'][$currencyId]['sum'] = bcadd((string) $this->report['sums'][$currencyId]['sum'], (string) $journal['amount']);
- // sum of spent:
- $this->report['sums'][$currencyId]['spent'] = -1 === bccomp((string) $journal['amount'], '0') ? bcadd(
- (string) $this->report['sums'][$currencyId]['spent'],
- (string) $journal['amount']
- ) : $this->report['sums'][$currencyId]['spent'];
- // sum of earned
- $this->report['sums'][$currencyId]['earned'] = 1 === bccomp((string) $journal['amount'], '0') ? bcadd(
- (string) $this->report['sums'][$currencyId]['earned'],
- (string) $journal['amount']
- ) : $this->report['sums'][$currencyId]['earned'];
-
- // sum of category
- $this->report['categories'][$key]['sum'] = bcadd((string) $this->report['categories'][$key]['sum'], (string) $journal['amount']);
- // total spent in category
- $this->report['categories'][$key]['spent'] = -1 === bccomp((string) $journal['amount'], '0') ? bcadd(
- (string) $this->report['categories'][$key]['spent'],
- (string) $journal['amount']
- ) : $this->report['categories'][$key]['spent'];
- // total earned in category
- $this->report['categories'][$key]['earned'] = 1 === bccomp((string) $journal['amount'], '0') ? bcadd(
- (string) $this->report['categories'][$key]['earned'],
- (string) $journal['amount']
- ) : $this->report['categories'][$key]['earned'];
+ /**
+ * @var int $currencyId
+ * @var array $currencyRow
+ */
+ foreach ($data as $currencyId => $currencyRow) {
+ $this->processCurrencyArray($currencyId, $currencyRow);
}
}
-
- public function setAccounts(Collection $accounts): void
- {
- $this->accounts = $accounts;
- }
-
- public function setEnd(Carbon $end): void
- {
- $this->end = $end;
- }
-
- public function setStart(Carbon $start): void
- {
- $this->start = $start;
- }
-
- public function setUser(User $user): void
- {
- $this->noCatRepository->setUser($user);
- $this->opsRepository->setUser($user);
- }
}
diff --git a/app/Support/Report/Summarizer/TransactionSummarizer.php b/app/Support/Report/Summarizer/TransactionSummarizer.php
index aea25a5663..83f057c68c 100644
--- a/app/Support/Report/Summarizer/TransactionSummarizer.php
+++ b/app/Support/Report/Summarizer/TransactionSummarizer.php
@@ -43,13 +43,6 @@ class TransactionSummarizer
}
}
- public function setUser(User $user): void
- {
- $this->user = $user;
- $this->default = Amount::getPrimaryCurrencyByUserGroup($user->userGroup);
- $this->convertToPrimary = Amount::convertToPrimary($user);
- }
-
public function groupByCurrencyId(array $journals, string $method = 'negative', bool $includeForeign = true): array
{
Log::debug(sprintf('Now in groupByCurrencyId([%d journals], "%s", %s)', count($journals), $method, var_export($includeForeign, true)));
@@ -58,7 +51,7 @@ class TransactionSummarizer
$field = 'amount';
// grab default currency information.
- $currencyId = (int) $journal['currency_id'];
+ $currencyId = (int)$journal['currency_id'];
$currencyName = $journal['currency_name'];
$currencySymbol = $journal['currency_symbol'];
$currencyCode = $journal['currency_code'];
@@ -74,8 +67,8 @@ class TransactionSummarizer
if ($this->convertToPrimary) {
// Log::debug('convertToPrimary is true.');
// if convert to primary currency, use the primary currency amount yes or no?
- $usePrimary = $this->default->id !== (int) $journal['currency_id'];
- $useForeign = $this->default->id === (int) $journal['foreign_currency_id'];
+ $usePrimary = $this->default->id !== (int)$journal['currency_id'];
+ $useForeign = $this->default->id === (int)$journal['foreign_currency_id'];
if ($usePrimary) {
// Log::debug(sprintf('Journal #%d switches to primary currency amount (original is %s)', $journal['transaction_journal_id'], $journal['currency_code']));
$field = 'pc_amount';
@@ -88,7 +81,7 @@ class TransactionSummarizer
if ($useForeign) {
// Log::debug(sprintf('Journal #%d switches to foreign amount (foreign is %s)', $journal['transaction_journal_id'], $journal['foreign_currency_code']));
$field = 'foreign_amount';
- $currencyId = (int) $journal['foreign_currency_id'];
+ $currencyId = (int)$journal['foreign_currency_id'];
$currencyName = $journal['foreign_currency_name'];
$currencySymbol = $journal['foreign_currency_symbol'];
$currencyCode = $journal['foreign_currency_code'];
@@ -98,7 +91,7 @@ class TransactionSummarizer
if (!$this->convertToPrimary) {
// Log::debug('convertToPrimary is false.');
// use foreign amount?
- $foreignCurrencyId = (int) $journal['foreign_currency_id'];
+ $foreignCurrencyId = (int)$journal['foreign_currency_id'];
if (0 !== $foreignCurrencyId) {
Log::debug(sprintf('Journal #%d also includes foreign amount (foreign is "%s")', $journal['transaction_journal_id'], $journal['foreign_currency_code']));
$foreignCurrencyName = $journal['foreign_currency_name'];
@@ -109,7 +102,7 @@ class TransactionSummarizer
}
// first process normal amount
- $amount = (string) ($journal[$field] ?? '0');
+ $amount = (string)($journal[$field] ?? '0');
$array[$currencyId] ??= [
'sum' => '0',
'currency_id' => $currencyId,
@@ -128,7 +121,7 @@ class TransactionSummarizer
// then process foreign amount, if it exists.
if (0 !== $foreignCurrencyId && true === $includeForeign) {
- $amount = (string) ($journal['foreign_amount'] ?? '0');
+ $amount = (string)($journal['foreign_amount'] ?? '0');
$array[$foreignCurrencyId] ??= [
'sum' => '0',
'currency_id' => $foreignCurrencyId,
@@ -200,12 +193,12 @@ class TransactionSummarizer
];
// add the data from the $field to the array.
- $array[$key]['sum'] = bcadd($array[$key]['sum'], Steam::{$method}((string) ($journal[$field] ?? '0'))); // @phpstan-ignore-line
+ $array[$key]['sum'] = bcadd($array[$key]['sum'], (string) Steam::{$method}((string)($journal[$field] ?? '0'))); // @phpstan-ignore-line
Log::debug(sprintf('Field for transaction #%d is "%s" (%s). Sum: %s', $journal['transaction_group_id'], $currencyCode, $field, $array[$key]['sum']));
// also do foreign amount, but only when convertToPrimary is false (otherwise we have it already)
// or when convertToPrimary is true and the foreign currency is ALSO not the default currency.
- if ((!$convertToPrimary || $journal['foreign_currency_id'] !== $primary->id) && 0 !== (int) $journal['foreign_currency_id']) {
+ if ((!$convertToPrimary || $journal['foreign_currency_id'] !== $primary->id) && 0 !== (int)$journal['foreign_currency_id']) {
Log::debug(sprintf('Use foreign amount from transaction #%d: %s %s. Sum: %s', $journal['transaction_group_id'], $currencyCode, $journal['foreign_amount'], $array[$key]['sum']));
$key = sprintf('%s-%s', $journal[$idKey], $journal['foreign_currency_id']);
$array[$key] ??= [
@@ -218,7 +211,7 @@ class TransactionSummarizer
'currency_code' => $journal['foreign_currency_code'],
'currency_decimal_places' => $journal['foreign_currency_decimal_places'],
];
- $array[$key]['sum'] = bcadd($array[$key]['sum'], Steam::{$method}((string) $journal['foreign_amount'])); // @phpstan-ignore-line
+ $array[$key]['sum'] = bcadd($array[$key]['sum'], (string) Steam::{$method}((string)$journal['foreign_amount'])); // @phpstan-ignore-line
}
}
@@ -230,4 +223,11 @@ class TransactionSummarizer
Log::debug(sprintf('Overrule convertToPrimary to become %s', var_export($convertToPrimary, true)));
$this->convertToPrimary = $convertToPrimary;
}
+
+ public function setUser(User $user): void
+ {
+ $this->user = $user;
+ $this->default = Amount::getPrimaryCurrencyByUserGroup($user->userGroup);
+ $this->convertToPrimary = Amount::convertToPrimary($user);
+ }
}
diff --git a/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php b/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php
index 439fd1f50a..df73a72f60 100644
--- a/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php
+++ b/app/Support/Repositories/Recurring/CalculateRangeOccurrences.php
@@ -58,7 +58,7 @@ trait CalculateRangeOccurrences
{
$return = [];
$attempts = 0;
- $dayOfMonth = (int) $moment;
+ $dayOfMonth = (int)$moment;
if ($start->day > $dayOfMonth) {
// day has passed already, add a month.
$start->addMonth();
@@ -113,7 +113,7 @@ trait CalculateRangeOccurrences
app('log')->debug('Rep is weekly.');
// monday = 1
// sunday = 7
- $dayOfWeek = (int) $moment;
+ $dayOfWeek = (int)$moment;
app('log')->debug(sprintf('DoW in repetition is %d, in mutator is %d', $dayOfWeek, $start->dayOfWeekIso));
if ($start->dayOfWeekIso > $dayOfWeek) {
// day has already passed this week, add one week:
diff --git a/app/Support/Repositories/Recurring/CalculateXOccurrences.php b/app/Support/Repositories/Recurring/CalculateXOccurrences.php
index 04f07046d4..f31171810f 100644
--- a/app/Support/Repositories/Recurring/CalculateXOccurrences.php
+++ b/app/Support/Repositories/Recurring/CalculateXOccurrences.php
@@ -63,7 +63,7 @@ trait CalculateXOccurrences
$mutator = clone $date;
$total = 0;
$attempts = 0;
- $dayOfMonth = (int) $moment;
+ $dayOfMonth = (int)$moment;
if ($mutator->day > $dayOfMonth) {
// day has passed already, add a month.
$mutator->addMonth();
@@ -127,7 +127,7 @@ trait CalculateXOccurrences
// monday = 1
// sunday = 7
$mutator->addDay(); // always assume today has passed.
- $dayOfWeek = (int) $moment;
+ $dayOfWeek = (int)$moment;
if ($mutator->dayOfWeekIso > $dayOfWeek) {
// day has already passed this week, add one week:
$mutator->addWeek();
diff --git a/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php b/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php
index 215c11bf0a..2fc216493d 100644
--- a/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php
+++ b/app/Support/Repositories/Recurring/CalculateXOccurrencesSince.php
@@ -68,7 +68,7 @@ trait CalculateXOccurrencesSince
$mutator = clone $date;
$total = 0;
$attempts = 0;
- $dayOfMonth = (int) $moment;
+ $dayOfMonth = (int)$moment;
$dayOfMonth = 0 === $dayOfMonth ? 1 : $dayOfMonth;
if ($mutator->day > $dayOfMonth) {
Log::debug(sprintf('%d is after %d, add a month. Mutator is now...', $mutator->day, $dayOfMonth));
@@ -145,7 +145,7 @@ trait CalculateXOccurrencesSince
// sunday = 7
// Removed assumption today has passed, see issue https://github.com/firefly-iii/firefly-iii/issues/4798
// $mutator->addDay(); // always assume today has passed.
- $dayOfWeek = (int) $moment;
+ $dayOfWeek = (int)$moment;
if ($mutator->dayOfWeekIso > $dayOfWeek) {
// day has already passed this week, add one week:
$mutator->addWeek();
diff --git a/app/Support/Request/AppendsLocationData.php b/app/Support/Request/AppendsLocationData.php
index 149898f986..b77cd9ee41 100644
--- a/app/Support/Request/AppendsLocationData.php
+++ b/app/Support/Request/AppendsLocationData.php
@@ -46,19 +46,17 @@ trait AppendsLocationData
return $return;
}
- private function validLongitude(string $longitude): bool
- {
- $number = (float) $longitude;
-
- return $number >= -180 && $number <= 180;
- }
-
- private function validLatitude(string $latitude): bool
- {
- $number = (float) $latitude;
-
- return $number >= -90 && $number <= 90;
- }
+ /**
+ * Abstract method stolen from "InteractsWithInput".
+ *
+ * @param null $key
+ * @param bool $default
+ *
+ * @return mixed
+ *
+ * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
+ */
+ abstract public function boolean($key = null, $default = false);
/**
* Abstract method.
@@ -69,6 +67,22 @@ trait AppendsLocationData
*/
abstract public function has($key);
+ /**
+ * Abstract method.
+ *
+ * @return string
+ */
+ abstract public function method();
+
+ /**
+ * Abstract method.
+ *
+ * @param mixed ...$patterns
+ *
+ * @return mixed
+ */
+ abstract public function routeIs(...$patterns);
+
/**
* Read the submitted Request data and add new or updated Location data to the array.
*/
@@ -132,72 +146,22 @@ trait AppendsLocationData
return sprintf('%s_%s', $prefix, $key);
}
- private function isValidPost(?string $prefix): bool
+ private function isValidEmptyPUT(?string $prefix): bool
{
- app('log')->debug('Now in isValidPost()');
- $longitudeKey = $this->getLocationKey($prefix, 'longitude');
- $latitudeKey = $this->getLocationKey($prefix, 'latitude');
- $zoomLevelKey = $this->getLocationKey($prefix, 'zoom_level');
- $hasLocationKey = $this->getLocationKey($prefix, 'has_location');
- // fields must not be null:
- if (null !== $this->get($longitudeKey) && null !== $this->get($latitudeKey) && null !== $this->get($zoomLevelKey)) {
- app('log')->debug('All fields present');
- // if is POST and route contains API, this is enough:
- if ('POST' === $this->method() && $this->routeIs('api.v1.*')) {
- app('log')->debug('Is API location');
+ $longitudeKey = $this->getLocationKey($prefix, 'longitude');
+ $latitudeKey = $this->getLocationKey($prefix, 'latitude');
+ $zoomLevelKey = $this->getLocationKey($prefix, 'zoom_level');
- return true;
- }
- // if is POST and route does not contain API, must also have "has_location" = true
- if ('POST' === $this->method() && $this->routeIs('*.store') && !$this->routeIs('api.v1.*') && '' !== $hasLocationKey) {
- app('log')->debug('Is POST + store route.');
- $hasLocation = $this->boolean($hasLocationKey);
- if (true === $hasLocation) {
- app('log')->debug('Has form form location');
-
- return true;
- }
- app('log')->debug('Does not have form location');
-
- return false;
- }
- app('log')->debug('Is not POST API or POST form');
-
- return false;
- }
- app('log')->debug('Fields not present');
-
- return false;
+ return (
+ null === $this->get($longitudeKey)
+ && null === $this->get($latitudeKey)
+ && null === $this->get($zoomLevelKey))
+ && (
+ 'PUT' === $this->method()
+ || ('POST' === $this->method() && $this->routeIs('*.update'))
+ );
}
- /**
- * Abstract method.
- *
- * @return string
- */
- abstract public function method();
-
- /**
- * Abstract method.
- *
- * @param mixed ...$patterns
- *
- * @return mixed
- */
- abstract public function routeIs(...$patterns);
-
- /**
- * Abstract method stolen from "InteractsWithInput".
- *
- * @param null $key
- * @param bool $default
- *
- * @return mixed
- *
- * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
- */
- abstract public function boolean($key = null, $default = false);
-
private function isValidPUT(?string $prefix): bool
{
$longitudeKey = $this->getLocationKey($prefix, 'longitude');
@@ -238,19 +202,55 @@ trait AppendsLocationData
return false;
}
- private function isValidEmptyPUT(?string $prefix): bool
+ private function isValidPost(?string $prefix): bool
{
- $longitudeKey = $this->getLocationKey($prefix, 'longitude');
- $latitudeKey = $this->getLocationKey($prefix, 'latitude');
- $zoomLevelKey = $this->getLocationKey($prefix, 'zoom_level');
+ app('log')->debug('Now in isValidPost()');
+ $longitudeKey = $this->getLocationKey($prefix, 'longitude');
+ $latitudeKey = $this->getLocationKey($prefix, 'latitude');
+ $zoomLevelKey = $this->getLocationKey($prefix, 'zoom_level');
+ $hasLocationKey = $this->getLocationKey($prefix, 'has_location');
+ // fields must not be null:
+ if (null !== $this->get($longitudeKey) && null !== $this->get($latitudeKey) && null !== $this->get($zoomLevelKey)) {
+ app('log')->debug('All fields present');
+ // if is POST and route contains API, this is enough:
+ if ('POST' === $this->method() && $this->routeIs('api.v1.*')) {
+ app('log')->debug('Is API location');
- return (
- null === $this->get($longitudeKey)
- && null === $this->get($latitudeKey)
- && null === $this->get($zoomLevelKey))
- && (
- 'PUT' === $this->method()
- || ('POST' === $this->method() && $this->routeIs('*.update'))
- );
+ return true;
+ }
+ // if is POST and route does not contain API, must also have "has_location" = true
+ if ('POST' === $this->method() && $this->routeIs('*.store') && !$this->routeIs('api.v1.*') && '' !== $hasLocationKey) {
+ app('log')->debug('Is POST + store route.');
+ $hasLocation = $this->boolean($hasLocationKey);
+ if (true === $hasLocation) {
+ app('log')->debug('Has form form location');
+
+ return true;
+ }
+ app('log')->debug('Does not have form location');
+
+ return false;
+ }
+ app('log')->debug('Is not POST API or POST form');
+
+ return false;
+ }
+ app('log')->debug('Fields not present');
+
+ return false;
+ }
+
+ private function validLatitude(string $latitude): bool
+ {
+ $number = (float)$latitude;
+
+ return $number >= -90 && $number <= 90;
+ }
+
+ private function validLongitude(string $longitude): bool
+ {
+ $number = (float)$longitude;
+
+ return $number >= -180 && $number <= 180;
}
}
diff --git a/app/Support/Request/ChecksLogin.php b/app/Support/Request/ChecksLogin.php
index 9fd2e11883..0f9753ee62 100644
--- a/app/Support/Request/ChecksLogin.php
+++ b/app/Support/Request/ChecksLogin.php
@@ -86,10 +86,10 @@ trait ChecksLogin
$userGroup = $this->route()?->parameter('userGroup');
if (null === $userGroup) {
app('log')->debug('Request class has no userGroup parameter, but perhaps there is a parameter.');
- $userGroupId = (int) $this->get('user_group_id');
+ $userGroupId = (int)$this->get('user_group_id');
if (0 === $userGroupId) {
app('log')->debug(sprintf('Request class has no user_group_id parameter, grab default from user (group #%d).', $user->user_group_id));
- $userGroupId = (int) $user->user_group_id;
+ $userGroupId = (int)$user->user_group_id;
}
$userGroup = UserGroup::find($userGroupId);
if (null === $userGroup) {
diff --git a/app/Support/Request/ConvertsDataTypes.php b/app/Support/Request/ConvertsDataTypes.php
index c90541b081..5293a6f804 100644
--- a/app/Support/Request/ConvertsDataTypes.php
+++ b/app/Support/Request/ConvertsDataTypes.php
@@ -99,28 +99,6 @@ trait ConvertsDataTypes
return Steam::filterSpaces($string);
}
- public function convertSortParameters(string $field, string $class): array
- {
- // assume this all works, because the validator would have caught any errors.
- $parameter = (string)request()->query->get($field);
- if ('' === $parameter) {
- return [];
- }
- $parts = explode(',', $parameter);
- $sortParameters = [];
- foreach ($parts as $part) {
- $part = trim($part);
- $direction = 'asc';
- if ('-' === $part[0]) {
- $part = substr($part, 1);
- $direction = 'desc';
- }
- $sortParameters[] = [$part, $direction];
- }
-
- return $sortParameters;
- }
-
public function clearString(?string $string): ?string
{
$string = $this->clearStringKeepNewlines($string);
@@ -159,6 +137,36 @@ trait ConvertsDataTypes
return Steam::filterSpaces($this->convertString($field));
}
+ /**
+ * Return integer value.
+ */
+ public function convertInteger(string $field): int
+ {
+ return (int)$this->get($field);
+ }
+
+ public function convertSortParameters(string $field, string $class): array
+ {
+ // assume this all works, because the validator would have caught any errors.
+ $parameter = (string)request()->query->get($field);
+ if ('' === $parameter) {
+ return [];
+ }
+ $parts = explode(',', $parameter);
+ $sortParameters = [];
+ foreach ($parts as $part) {
+ $part = trim($part);
+ $direction = 'asc';
+ if ('-' === $part[0]) {
+ $part = substr($part, 1);
+ $direction = 'desc';
+ }
+ $sortParameters[] = [$part, $direction];
+ }
+
+ return $sortParameters;
+ }
+
/**
* Return string value.
*/
@@ -178,14 +186,6 @@ trait ConvertsDataTypes
*/
abstract public function get(string $key, mixed $default = null): mixed;
- /**
- * Return integer value.
- */
- public function convertInteger(string $field): int
- {
- return (int)$this->get($field);
- }
-
/**
* TODO duplicate, see SelectTransactionsRequest
*
@@ -218,6 +218,16 @@ trait ConvertsDataTypes
return $collection;
}
+ /**
+ * Abstract method that always exists in the Request classes that use this
+ * trait, OR a stub needs to be added by any other class that uses this train.
+ *
+ * @param mixed $key
+ *
+ * @return mixed
+ */
+ abstract public function has($key);
+
/**
* Return string value with newlines.
*/
@@ -258,6 +268,12 @@ trait ConvertsDataTypes
if ('yes' === $value) {
return true;
}
+ if ('on' === $value) {
+ return true;
+ }
+ if ('y' === $value) {
+ return true;
+ }
if ('1' === $value) {
return true;
}
@@ -380,16 +396,6 @@ trait ConvertsDataTypes
return $return;
}
- /**
- * Abstract method that always exists in the Request classes that use this
- * trait, OR a stub needs to be added by any other class that uses this train.
- *
- * @param mixed $key
- *
- * @return mixed
- */
- abstract public function has($key);
-
/**
* Return date or NULL.
*/
@@ -412,6 +418,21 @@ trait ConvertsDataTypes
return $result;
}
+ /**
+ * Parse to integer
+ */
+ protected function integerFromValue(?string $string): ?int
+ {
+ if (null === $string) {
+ return null;
+ }
+ if ('' === $string) {
+ return null;
+ }
+
+ return (int)$string;
+ }
+
/**
* Return integer value, or NULL when it's not set.
*/
@@ -457,19 +478,4 @@ trait ConvertsDataTypes
return $return;
}
-
- /**
- * Parse to integer
- */
- protected function integerFromValue(?string $string): ?int
- {
- if (null === $string) {
- return null;
- }
- if ('' === $string) {
- return null;
- }
-
- return (int)$string;
- }
}
diff --git a/app/Support/Request/GetRecurrenceData.php b/app/Support/Request/GetRecurrenceData.php
index 50dc0cd8f2..40f23738dd 100644
--- a/app/Support/Request/GetRecurrenceData.php
+++ b/app/Support/Request/GetRecurrenceData.php
@@ -38,12 +38,12 @@ trait GetRecurrenceData
foreach ($stringKeys as $key) {
if (array_key_exists($key, $transaction)) {
- $return[$key] = (string) $transaction[$key];
+ $return[$key] = (string)$transaction[$key];
}
}
foreach ($intKeys as $key) {
if (array_key_exists($key, $transaction)) {
- $return[$key] = (int) $transaction[$key];
+ $return[$key] = (int)$transaction[$key];
}
}
foreach ($keys as $key) {
diff --git a/app/Support/Request/ValidatesWebhooks.php b/app/Support/Request/ValidatesWebhooks.php
index 5647184ef4..15d41f41ef 100644
--- a/app/Support/Request/ValidatesWebhooks.php
+++ b/app/Support/Request/ValidatesWebhooks.php
@@ -25,10 +25,10 @@ declare(strict_types=1);
namespace FireflyIII\Support\Request;
-use Illuminate\Validation\Validator;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Models\Webhook;
use Illuminate\Support\Facades\Log;
+use Illuminate\Validation\Validator;
trait ValidatesWebhooks
{
diff --git a/app/Support/Search/AccountSearch.php b/app/Support/Search/AccountSearch.php
index 99b89f9c99..44bb34e893 100644
--- a/app/Support/Search/AccountSearch.php
+++ b/app/Support/Search/AccountSearch.php
@@ -92,7 +92,7 @@ class AccountSearch implements GenericSearchInterface
break;
case self::SEARCH_ID:
- $searchQuery->where('accounts.id', '=', (int) $originalQuery);
+ $searchQuery->where('accounts.id', '=', (int)$originalQuery);
break;
diff --git a/app/Support/Search/OperatorQuerySearch.php b/app/Support/Search/OperatorQuerySearch.php
index dc975609f0..5b2cc0776c 100644
--- a/app/Support/Search/OperatorQuerySearch.php
+++ b/app/Support/Search/OperatorQuerySearch.php
@@ -105,6 +105,41 @@ class OperatorQuerySearch implements SearchInterface
$this->currencyRepository = app(CurrencyRepositoryInterface::class);
}
+ /**
+ * @throws FireflyException
+ */
+ public static function getRootOperator(string $operator): string
+ {
+ $original = $operator;
+ // if the string starts with "-" (not), we can remove it and recycle
+ // the configuration from the original operator.
+ if (str_starts_with($operator, '-')) {
+ $operator = substr($operator, 1);
+ }
+
+ $config = config(sprintf('search.operators.%s', $operator));
+ if (null === $config) {
+ throw new FireflyException(sprintf('No configuration for search operator "%s"', $operator));
+ }
+ if (true === $config['alias']) {
+ $return = $config['alias_for'];
+ if (str_starts_with($original, '-')) {
+ $return = sprintf('-%s', $config['alias_for']);
+ }
+ Log::debug(sprintf('"%s" is an alias for "%s", so return that instead.', $original, $return));
+
+ return $return;
+ }
+ Log::debug(sprintf('"%s" is not an alias.', $operator));
+
+ return $original;
+ }
+
+ public function getExcludedWords(): array
+ {
+ return $this->prohibitedWords;
+ }
+
public function getInvalidOperators(): array
{
return $this->invalidOperators;
@@ -120,6 +155,11 @@ class OperatorQuerySearch implements SearchInterface
return $this->operators;
}
+ public function getWords(): array
+ {
+ return $this->words;
+ }
+
public function getWordsAsString(): string
{
return implode(' ', $this->words);
@@ -163,6 +203,124 @@ class OperatorQuerySearch implements SearchInterface
$this->collector->excludeSearchWords($this->prohibitedWords);
}
+ public function searchTime(): float
+ {
+ return microtime(true) - $this->startTime;
+ }
+
+ public function searchTransactions(): LengthAwarePaginator
+ {
+ $this->parseTagInstructions();
+ if (0 === count($this->getWords()) && 0 === count($this->getExcludedWords()) && 0 === count($this->getOperators())) {
+ return new LengthAwarePaginator([], 0, 5, 1);
+ }
+
+ return $this->collector->getPaginatedGroups();
+ }
+
+ public function setDate(Carbon $date): void
+ {
+ $this->date = $date;
+ }
+
+ public function setLimit(int $limit): void
+ {
+ $this->limit = $limit;
+ $this->collector->setLimit($this->limit);
+ }
+
+ public function setPage(int $page): void
+ {
+ $this->page = $page;
+ $this->collector->setPage($this->page);
+ }
+
+ public function setUser(User $user): void
+ {
+ $this->accountRepository->setUser($user);
+ $this->billRepository->setUser($user);
+ $this->categoryRepository->setUser($user);
+ $this->budgetRepository->setUser($user);
+ $this->tagRepository->setUser($user);
+ $this->collector = app(GroupCollectorInterface::class);
+ $this->collector->setUser($user);
+ $this->collector->withAccountInformation()->withCategoryInformation()->withBudgetInformation();
+
+ $this->setLimit((int)app('preferences')->getForUser($user, 'listPageSize', 50)->data);
+ }
+
+ private function findCurrency(string $value): ?TransactionCurrency
+ {
+ if (str_contains($value, '(') && str_contains($value, ')')) {
+ // bad method to split and get the currency code:
+ $parts = explode(' ', $value);
+ $value = trim($parts[count($parts) - 1], "() \t\n\r\0\x0B");
+ }
+ $result = $this->currencyRepository->findByCode($value);
+ if (null === $result) {
+ return $this->currencyRepository->findByName($value);
+ }
+
+ return $result;
+ }
+
+ private function getCashAccount(): Account
+ {
+ return $this->accountRepository->getCashAccount();
+ }
+
+ /**
+ * @throws FireflyException
+ */
+ private function handleFieldNode(FieldNode $node, bool $flipProhibitedFlag): void
+ {
+ $operator = strtolower($node->getOperator());
+ $value = $node->getValue();
+ $prohibited = $node->isProhibited($flipProhibitedFlag);
+
+ $context = config(sprintf('search.operators.%s.needs_context', $operator));
+
+ // is an operator that needs no context, and value is false, then prohibited = true.
+ if ('false' === $value && in_array($operator, $this->validOperators, true) && false === $context && !$prohibited) {
+ $prohibited = true;
+ $value = 'true';
+ }
+ // if the operator is prohibited, but the value is false, do an uno reverse
+ if ('false' === $value && $prohibited && in_array($operator, $this->validOperators, true) && false === $context) {
+ $prohibited = false;
+ $value = 'true';
+ }
+
+ // must be valid operator:
+ $inArray = in_array($operator, $this->validOperators, true);
+ if ($inArray) {
+ if ($this->updateCollector($operator, $value, $prohibited)) {
+ $this->operators->push([
+ 'type' => self::getRootOperator($operator),
+ 'value' => $value,
+ 'prohibited' => $prohibited,
+ ]);
+ Log::debug(sprintf('Added operator type "%s"', $operator));
+ }
+ }
+ if (!$inArray) {
+ Log::debug(sprintf('Added INVALID operator type "%s"', $operator));
+ $this->invalidOperators[] = [
+ 'type' => $operator,
+ 'value' => $value,
+ ];
+ }
+ }
+
+ private function handleNodeGroup(NodeGroup $node, bool $flipProhibitedFlag): void
+ {
+ $prohibited = $node->isProhibited($flipProhibitedFlag);
+
+ foreach ($node->getNodes() as $subNode) {
+ $this->handleSearchNode($subNode, $prohibited);
+ }
+ }
+
/**
* @throws FireflyException
*
@@ -214,43 +372,857 @@ class OperatorQuerySearch implements SearchInterface
/**
* @throws FireflyException
*/
- private function handleFieldNode(FieldNode $node, bool $flipProhibitedFlag): void
+ private function parseDateRange(string $type, string $value): array
{
- $operator = strtolower($node->getOperator());
- $value = $node->getValue();
- $prohibited = $node->isProhibited($flipProhibitedFlag);
-
- $context = config(sprintf('search.operators.%s.needs_context', $operator));
-
- // is an operator that needs no context, and value is false, then prohibited = true.
- if ('false' === $value && in_array($operator, $this->validOperators, true) && false === $context && !$prohibited) {
- $prohibited = true;
- $value = 'true';
- }
- // if the operator is prohibited, but the value is false, do an uno reverse
- if ('false' === $value && $prohibited && in_array($operator, $this->validOperators, true) && false === $context) {
- $prohibited = false;
- $value = 'true';
+ $parser = new ParseDateString();
+ if ($parser->isDateRange($value)) {
+ return $parser->parseRange($value);
}
- // must be valid operator:
- $inArray = in_array($operator, $this->validOperators, true);
- if ($inArray) {
- if ($this->updateCollector($operator, $value, $prohibited)) {
- $this->operators->push([
- 'type' => self::getRootOperator($operator),
- 'value' => $value,
- 'prohibited' => $prohibited,
- ]);
- Log::debug(sprintf('Added operator type "%s"', $operator));
- }
- }
- if (!$inArray) {
- Log::debug(sprintf('Added INVALID operator type "%s"', $operator));
+ try {
+ $parsedDate = $parser->parseDate($value);
+ } catch (FireflyException) {
+ Log::debug(sprintf('Could not parse date "%s", will return empty array.', $value));
$this->invalidOperators[] = [
- 'type' => $operator,
+ 'type' => $type,
'value' => $value,
];
+
+ return [];
+ }
+
+ return [
+ 'exact' => $parsedDate,
+ ];
+ }
+
+ private function parseTagInstructions(): void
+ {
+ Log::debug('Now in parseTagInstructions()');
+ // if exclude tags, remove excluded tags.
+ if (count($this->excludeTags) > 0) {
+ Log::debug(sprintf('%d exclude tag(s)', count($this->excludeTags)));
+ $collection = new Collection();
+ foreach ($this->excludeTags as $tagId) {
+ $tag = $this->tagRepository->find($tagId);
+ if (null !== $tag) {
+ Log::debug(sprintf('Exclude tag "%s"', $tag->tag));
+ $collection->push($tag);
+ }
+ }
+ Log::debug(sprintf('Selecting all tags except %d excluded tag(s).', $collection->count()));
+ $this->collector->setWithoutSpecificTags($collection);
+ }
+ // if include tags, include them:
+ if (count($this->includeTags) > 0) {
+ Log::debug(sprintf('%d include tag(s)', count($this->includeTags)));
+ $collection = new Collection();
+ foreach ($this->includeTags as $tagId) {
+ $tag = $this->tagRepository->find($tagId);
+ if (null !== $tag) {
+ Log::debug(sprintf('Include tag "%s"', $tag->tag));
+ $collection->push($tag);
+ }
+ }
+ $this->collector->setAllTags($collection);
+ }
+ // if include ANY tags, include them: (see #8632)
+ if (count($this->includeAnyTags) > 0) {
+ Log::debug(sprintf('%d include ANY tag(s)', count($this->includeAnyTags)));
+ $collection = new Collection();
+ foreach ($this->includeAnyTags as $tagId) {
+ $tag = $this->tagRepository->find($tagId);
+ if (null !== $tag) {
+ Log::debug(sprintf('Include ANY tag "%s"', $tag->tag));
+ $collection->push($tag);
+ }
+ }
+ $this->collector->setTags($collection);
+ }
+ }
+
+ /**
+ * searchDirection: 1 = source (default), 2 = destination, 3 = both
+ * stringPosition: 1 = start (default), 2 = end, 3 = contains, 4 = is
+ *
+ * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
+ * @SuppressWarnings("PHPMD.NPathComplexity")
+ */
+ private function searchAccount(string $value, SearchDirection $searchDirection, StringPosition $stringPosition, bool $prohibited = false): void
+ {
+ Log::debug(sprintf('searchAccount("%s", %s, %s)', $value, $stringPosition->name, $searchDirection->name));
+
+ // search direction (default): for source accounts
+ $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::REVENUE->value];
+ $collectorMethod = 'setSourceAccounts';
+ if ($prohibited) {
+ $collectorMethod = 'excludeSourceAccounts';
+ }
+
+ // search direction: for destination accounts
+ if (SearchDirection::DESTINATION === $searchDirection) { // destination
+ // destination can be
+ $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value];
+ $collectorMethod = 'setDestinationAccounts';
+ if ($prohibited) {
+ $collectorMethod = 'excludeDestinationAccounts';
+ }
+ }
+ // either account could be:
+ if (SearchDirection::BOTH === $searchDirection) {
+ $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value, AccountTypeEnum::REVENUE->value];
+ $collectorMethod = 'setAccounts';
+ if ($prohibited) {
+ $collectorMethod = 'excludeAccounts';
+ }
+ }
+ // string position (default): starts with:
+ $stringMethod = 'str_starts_with';
+
+ // string position: ends with:
+ if (StringPosition::ENDS === $stringPosition) {
+ $stringMethod = 'str_ends_with';
+ }
+ if (StringPosition::CONTAINS === $stringPosition) {
+ $stringMethod = 'str_contains';
+ }
+ if (StringPosition::IS === $stringPosition) {
+ $stringMethod = 'stringIsEqual';
+ }
+
+ // get accounts:
+ $accounts = $this->accountRepository->searchAccount($value, $searchTypes, 1337);
+ if (0 === $accounts->count() && false === $prohibited) {
+ Log::warning('Found zero accounts, search for non existing account, NO results will be returned.');
+ $this->collector->findNothing();
+
+ return;
+ }
+ if (0 === $accounts->count() && true === $prohibited) {
+ Log::debug('Found zero accounts, but the search is negated, so effectively we ignore the search parameter.');
+
+ return;
+ }
+ Log::debug(sprintf('Found %d accounts, will filter.', $accounts->count()));
+ $filtered = $accounts->filter(
+ static fn (Account $account) => $stringMethod(strtolower($account->name), strtolower($value))
+ );
+
+ if (0 === $filtered->count()) {
+ Log::warning('Left with zero accounts, so cannot find anything, NO results will be returned.');
+ $this->collector->findNothing();
+
+ return;
+ }
+ Log::debug(sprintf('Left with %d, set as %s().', $filtered->count(), $collectorMethod));
+ $this->collector->{$collectorMethod}($filtered); // @phpstan-ignore-line
+ }
+
+ /**
+ * TODO make enums
+ * searchDirection: 1 = source (default), 2 = destination, 3 = both
+ * stringPosition: 1 = start (default), 2 = end, 3 = contains, 4 = is
+ *
+ * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
+ * @SuppressWarnings("PHPMD.NPathComplexity")
+ */
+ private function searchAccountNr(string $value, SearchDirection $searchDirection, StringPosition $stringPosition, bool $prohibited = false): void
+ {
+ Log::debug(sprintf('searchAccountNr(%s, %d, %d)', $value, $searchDirection->name, $stringPosition->name));
+
+ // search direction (default): for source accounts
+ $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::REVENUE->value];
+ $collectorMethod = 'setSourceAccounts';
+ if (true === $prohibited) {
+ $collectorMethod = 'excludeSourceAccounts';
+ }
+
+ // search direction: for destination accounts
+ if (SearchDirection::DESTINATION === $searchDirection) {
+ // destination can be
+ $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value];
+ $collectorMethod = 'setDestinationAccounts';
+ if (true === $prohibited) {
+ $collectorMethod = 'excludeDestinationAccounts';
+ }
+ }
+
+ // either account could be:
+ if (SearchDirection::BOTH === $searchDirection) {
+ $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value, AccountTypeEnum::REVENUE->value];
+ $collectorMethod = 'setAccounts';
+ if (true === $prohibited) {
+ $collectorMethod = 'excludeAccounts';
+ }
+ }
+
+ // string position (default): starts with:
+ $stringMethod = 'str_starts_with';
+
+ // string position: ends with:
+ if (StringPosition::ENDS === $stringPosition) {
+ $stringMethod = 'str_ends_with';
+ }
+ if (StringPosition::CONTAINS === $stringPosition) {
+ $stringMethod = 'str_contains';
+ }
+ if (StringPosition::IS === $stringPosition) {
+ $stringMethod = 'stringIsEqual';
+ }
+
+ // search for accounts:
+ $accounts = $this->accountRepository->searchAccountNr($value, $searchTypes, 1337);
+ if (0 === $accounts->count()) {
+ Log::debug('Found zero accounts, search for invalid account.');
+ Log::warning('Call to findNothing() from searchAccountNr().');
+ $this->collector->findNothing();
+
+ return;
+ }
+
+ // if found, do filter
+ Log::debug(sprintf('Found %d accounts, will filter.', $accounts->count()));
+ $filtered = $accounts->filter(
+ static function (Account $account) use ($value, $stringMethod) {
+ // either IBAN or account number
+ $ibanMatch = $stringMethod(strtolower((string)$account->iban), strtolower($value));
+ $accountNrMatch = false;
+
+ /** @var AccountMeta $meta */
+ foreach ($account->accountMeta as $meta) {
+ if ('account_number' === $meta->name && $stringMethod(strtolower((string)$meta->data), strtolower($value))) {
+ $accountNrMatch = true;
+ }
+ }
+
+ return $ibanMatch || $accountNrMatch;
+ }
+ );
+
+ if (0 === $filtered->count()) {
+ Log::debug('Left with zero, search for invalid account');
+ Log::warning('Call to findNothing() from searchAccountNr().');
+ $this->collector->findNothing();
+
+ return;
+ }
+ Log::debug(sprintf('Left with %d, set as %s().', $filtered->count(), $collectorMethod));
+ $this->collector->{$collectorMethod}($filtered); // @phpstan-ignore-line
+ }
+
+ /**
+ * @throws FireflyException
+ *
+ * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
+ */
+ private function setDateAfterParams(array $range, bool $prohibited = false): void
+ {
+ /**
+ * @var string $key
+ * @var Carbon|string $value
+ */
+ foreach ($range as $key => $value) {
+ $key = $prohibited ? sprintf('%s_not', $key) : $key;
+
+ switch ($key) {
+ default:
+ throw new FireflyException(sprintf('Cannot handle key "%s" in setDateAfterParams()', $key));
+
+ case 'exact':
+ if ($value instanceof Carbon) {
+ $this->collector->setAfter($value);
+ $this->operators->push(['type' => 'date_after', 'value' => $value->format('Y-m-d')]);
+ }
+
+ break;
+
+ case 'year':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set date_is_after YEAR value "%s"', $value));
+ $this->collector->yearAfter($value);
+ $this->operators->push(['type' => 'date_after_year', 'value' => $value]);
+ }
+
+ break;
+
+ case 'month':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set date_is_after MONTH value "%s"', $value));
+ $this->collector->monthAfter($value);
+ $this->operators->push(['type' => 'date_after_month', 'value' => $value]);
+ }
+
+ break;
+
+ case 'day':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set date_is_after DAY value "%s"', $value));
+ $this->collector->dayAfter($value);
+ $this->operators->push(['type' => 'date_after_day', 'value' => $value]);
+ }
+
+ break;
+ }
+ }
+ }
+
+ /**
+ * @throws FireflyException
+ *
+ * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
+ */
+ private function setDateBeforeParams(array $range, bool $prohibited = false): void
+ {
+ /**
+ * @var string $key
+ * @var Carbon|string $value
+ */
+ foreach ($range as $key => $value) {
+ $key = $prohibited ? sprintf('%s_not', $key) : $key;
+
+ switch ($key) {
+ default:
+ throw new FireflyException(sprintf('Cannot handle key "%s" in setDateBeforeParams()', $key));
+
+ case 'exact':
+ if ($value instanceof Carbon) {
+ $this->collector->setBefore($value);
+ $this->operators->push(['type' => 'date_before', 'value' => $value->format('Y-m-d')]);
+ }
+
+ break;
+
+ case 'year':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set date_is_before YEAR value "%s"', $value));
+ $this->collector->yearBefore($value);
+ $this->operators->push(['type' => 'date_before_year', 'value' => $value]);
+ }
+
+ break;
+
+ case 'month':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set date_is_before MONTH value "%s"', $value));
+ $this->collector->monthBefore($value);
+ $this->operators->push(['type' => 'date_before_month', 'value' => $value]);
+ }
+
+ break;
+
+ case 'day':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set date_is_before DAY value "%s"', $value));
+ $this->collector->dayBefore($value);
+ $this->operators->push(['type' => 'date_before_day', 'value' => $value]);
+ }
+
+ break;
+ }
+ }
+ }
+
+ /**
+ * @throws FireflyException
+ *
+ * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
+ */
+ private function setExactDateParams(array $range, bool $prohibited = false): void
+ {
+ /**
+ * @var string $key
+ * @var Carbon|string $value
+ */
+ foreach ($range as $key => $value) {
+ $key = $prohibited ? sprintf('%s_not', $key) : $key;
+
+ switch ($key) {
+ default:
+ throw new FireflyException(sprintf('Cannot handle key "%s" in setExactParameters()', $key));
+
+ case 'exact':
+ if ($value instanceof Carbon) {
+ Log::debug(sprintf('Set date_is_exact value "%s"', $value->format('Y-m-d')));
+ $this->collector->setRange($value, $value);
+ $this->operators->push(['type' => 'date_on', 'value' => $value->format('Y-m-d')]);
+ }
+
+ break;
+
+ case 'exact_not':
+ if ($value instanceof Carbon) {
+ $this->collector->excludeRange($value, $value);
+ $this->operators->push(['type' => 'not_date_on', 'value' => $value->format('Y-m-d')]);
+ }
+
+ break;
+
+ case 'year':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set date_is_exact YEAR value "%s"', $value));
+ $this->collector->yearIs($value);
+ $this->operators->push(['type' => 'date_on_year', 'value' => $value]);
+ }
+
+ break;
+
+ case 'year_not':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set date_is_exact_not YEAR value "%s"', $value));
+ $this->collector->yearIsNot($value);
+ $this->operators->push(['type' => 'not_date_on_year', 'value' => $value]);
+ }
+
+ break;
+
+ case 'month':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set date_is_exact MONTH value "%s"', $value));
+ $this->collector->monthIs($value);
+ $this->operators->push(['type' => 'date_on_month', 'value' => $value]);
+ }
+
+ break;
+
+ case 'month_not':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set date_is_exact not MONTH value "%s"', $value));
+ $this->collector->monthIsNot($value);
+ $this->operators->push(['type' => 'not_date_on_month', 'value' => $value]);
+ }
+
+ break;
+
+ case 'day':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set date_is_exact DAY value "%s"', $value));
+ $this->collector->dayIs($value);
+ $this->operators->push(['type' => 'date_on_day', 'value' => $value]);
+ }
+
+ break;
+
+ case 'day_not':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set not date_is_exact DAY value "%s"', $value));
+ $this->collector->dayIsNot($value);
+ $this->operators->push(['type' => 'not_date_on_day', 'value' => $value]);
+ }
+
+ break;
+ }
+ }
+ }
+
+ /**
+ * @throws FireflyException
+ *
+ * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
+ */
+ private function setExactMetaDateParams(string $field, array $range, bool $prohibited = false): void
+ {
+ Log::debug('Now in setExactMetaDateParams()');
+
+ /**
+ * @var string $key
+ * @var Carbon|string $value
+ */
+ foreach ($range as $key => $value) {
+ $key = $prohibited ? sprintf('%s_not', $key) : $key;
+
+ switch ($key) {
+ default:
+ throw new FireflyException(sprintf('Cannot handle key "%s" in setExactMetaDateParams()', $key));
+
+ case 'exact':
+ if ($value instanceof Carbon) {
+ Log::debug(sprintf('Set %s_is_exact value "%s"', $field, $value->format('Y-m-d')));
+ $this->collector->setMetaDateRange($value, $value, $field);
+ $this->operators->push(['type' => sprintf('%s_on', $field), 'value' => $value->format('Y-m-d')]);
+ }
+
+ break;
+
+ case 'exact_not':
+ if ($value instanceof Carbon) {
+ Log::debug(sprintf('Set NOT %s_is_exact value "%s"', $field, $value->format('Y-m-d')));
+ $this->collector->excludeMetaDateRange($value, $value, $field);
+ $this->operators->push(['type' => sprintf('not_%s_on', $field), 'value' => $value->format('Y-m-d')]);
+ }
+
+ break;
+
+ case 'year':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set %s_is_exact YEAR value "%s"', $field, $value));
+ $this->collector->metaYearIs($value, $field);
+ $this->operators->push(['type' => sprintf('%s_on_year', $field), 'value' => $value]);
+ }
+
+ break;
+
+ case 'year_not':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set NOT %s_is_exact YEAR value "%s"', $field, $value));
+ $this->collector->metaYearIsNot($value, $field);
+ $this->operators->push(['type' => sprintf('not_%s_on_year', $field), 'value' => $value]);
+ }
+
+ break;
+
+ case 'month':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set %s_is_exact MONTH value "%s"', $field, $value));
+ $this->collector->metaMonthIs($value, $field);
+ $this->operators->push(['type' => sprintf('%s_on_month', $field), 'value' => $value]);
+ }
+
+ break;
+
+ case 'month_not':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set NOT %s_is_exact MONTH value "%s"', $field, $value));
+ $this->collector->metaMonthIsNot($value, $field);
+ $this->operators->push(['type' => sprintf('not_%s_on_month', $field), 'value' => $value]);
+ }
+
+ break;
+
+ case 'day':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set %s_is_exact DAY value "%s"', $field, $value));
+ $this->collector->metaDayIs($value, $field);
+ $this->operators->push(['type' => sprintf('%s_on_day', $field), 'value' => $value]);
+ }
+
+ break;
+
+ case 'day_not':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set NOT %s_is_exact DAY value "%s"', $field, $value));
+ $this->collector->metaDayIsNot($value, $field);
+ $this->operators->push(['type' => sprintf('not_%s_on_day', $field), 'value' => $value]);
+ }
+
+ break;
+ }
+ }
+ }
+
+ /**
+ * @throws FireflyException
+ *
+ * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
+ */
+ private function setExactObjectDateParams(string $field, array $range, bool $prohibited = false): void
+ {
+ /**
+ * @var string $key
+ * @var Carbon|string $value
+ */
+ foreach ($range as $key => $value) {
+ $key = $prohibited ? sprintf('%s_not', $key) : $key;
+
+ switch ($key) {
+ default:
+ throw new FireflyException(sprintf('Cannot handle key "%s" in setExactObjectDateParams()', $key));
+
+ case 'exact':
+ if ($value instanceof Carbon) {
+ Log::debug(sprintf('Set %s_is_exact value "%s"', $field, $value->format('Y-m-d')));
+ $this->collector->setObjectRange($value, clone $value, $field);
+ $this->operators->push(['type' => sprintf('%s_on', $field), 'value' => $value->format('Y-m-d')]);
+ }
+
+ break;
+
+ case 'exact_not':
+ if ($value instanceof Carbon) {
+ Log::debug(sprintf('Set NOT %s_is_exact value "%s"', $field, $value->format('Y-m-d')));
+ $this->collector->excludeObjectRange($value, clone $value, $field);
+ $this->operators->push(['type' => sprintf('not_%s_on', $field), 'value' => $value->format('Y-m-d')]);
+ }
+
+ break;
+
+ case 'year':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set %s_is_exact YEAR value "%s"', $field, $value));
+ $this->collector->objectYearIs($value, $field);
+ $this->operators->push(['type' => sprintf('%s_on_year', $field), 'value' => $value]);
+ }
+
+ break;
+
+ case 'year_not':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set NOT %s_is_exact YEAR value "%s"', $field, $value));
+ $this->collector->objectYearIsNot($value, $field);
+ $this->operators->push(['type' => sprintf('not_%s_on_year', $field), 'value' => $value]);
+ }
+
+ break;
+
+ case 'month':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set %s_is_exact MONTH value "%s"', $field, $value));
+ $this->collector->objectMonthIs($value, $field);
+ $this->operators->push(['type' => sprintf('%s_on_month', $field), 'value' => $value]);
+ }
+
+ break;
+
+ case 'month_not':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set NOT %s_is_exact MONTH value "%s"', $field, $value));
+ $this->collector->objectMonthIsNot($value, $field);
+ $this->operators->push(['type' => sprintf('not_%s_on_month', $field), 'value' => $value]);
+ }
+
+ break;
+
+ case 'day':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set %s_is_exact DAY value "%s"', $field, $value));
+ $this->collector->objectDayIs($value, $field);
+ $this->operators->push(['type' => sprintf('%s_on_day', $field), 'value' => $value]);
+ }
+
+ break;
+
+ case 'day_not':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set NOT %s_is_exact DAY value "%s"', $field, $value));
+ $this->collector->objectDayIsNot($value, $field);
+ $this->operators->push(['type' => sprintf('not_%s_on_day', $field), 'value' => $value]);
+ }
+
+ break;
+ }
+ }
+ }
+
+ /**
+ * @throws FireflyException
+ *
+ * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
+ */
+ private function setMetaDateAfterParams(string $field, array $range, bool $prohibited = false): void
+ {
+ /**
+ * @var string $key
+ * @var Carbon|string $value
+ */
+ foreach ($range as $key => $value) {
+ $key = $prohibited ? sprintf('%s_not', $key) : $key;
+
+ switch ($key) {
+ default:
+ throw new FireflyException(sprintf('Cannot handle key "%s" in setMetaDateAfterParams()', $key));
+
+ case 'exact':
+ if ($value instanceof Carbon) {
+ $this->collector->setMetaAfter($value, $field);
+ $this->operators->push(['type' => sprintf('%s_after', $field), 'value' => $value->format('Y-m-d')]);
+ }
+
+ break;
+
+ case 'year':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set %s_is_after YEAR value "%s"', $field, $value));
+ $this->collector->metaYearAfter($value, $field);
+ $this->operators->push(['type' => sprintf('%s_after_year', $field), 'value' => $value]);
+ }
+
+ break;
+
+ case 'month':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set %s_is_after MONTH value "%s"', $field, $value));
+ $this->collector->metaMonthAfter($value, $field);
+ $this->operators->push(['type' => sprintf('%s_after_month', $field), 'value' => $value]);
+ }
+
+ break;
+
+ case 'day':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set %s_is_after DAY value "%s"', $field, $value));
+ $this->collector->metaDayAfter($value, $field);
+ $this->operators->push(['type' => sprintf('%s_after_day', $field), 'value' => $value]);
+ }
+
+ break;
+ }
+ }
+ }
+
+ /**
+ * @throws FireflyException
+ *
+ * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
+ */
+ private function setMetaDateBeforeParams(string $field, array $range, bool $prohibited = false): void
+ {
+ /**
+ * @var string $key
+ * @var Carbon|string $value
+ */
+ foreach ($range as $key => $value) {
+ $key = $prohibited ? sprintf('%s_not', $key) : $key;
+
+ switch ($key) {
+ default:
+ throw new FireflyException(sprintf('Cannot handle key "%s" in setMetaDateBeforeParams()', $key));
+
+ case 'exact':
+ if ($value instanceof Carbon) {
+ $this->collector->setMetaBefore($value, $field);
+ $this->operators->push(['type' => sprintf('%s_before', $field), 'value' => $value->format('Y-m-d')]);
+ }
+
+ break;
+
+ case 'year':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set %s_is_before YEAR value "%s"', $field, $value));
+ $this->collector->metaYearBefore($value, $field);
+ $this->operators->push(['type' => sprintf('%s_before_year', $field), 'value' => $value]);
+ }
+
+ break;
+
+ case 'month':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set %s_is_before MONTH value "%s"', $field, $value));
+ $this->collector->metaMonthBefore($value, $field);
+ $this->operators->push(['type' => sprintf('%s_before_month', $field), 'value' => $value]);
+ }
+
+ break;
+
+ case 'day':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set %s_is_before DAY value "%s"', $field, $value));
+ $this->collector->metaDayBefore($value, $field);
+ $this->operators->push(['type' => sprintf('%s_before_day', $field), 'value' => $value]);
+ }
+
+ break;
+ }
+ }
+ }
+
+ /**
+ * @throws FireflyException
+ *
+ * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
+ */
+ private function setObjectDateAfterParams(string $field, array $range, bool $prohibited = false): void
+ {
+ /**
+ * @var string $key
+ * @var Carbon|string $value
+ */
+ foreach ($range as $key => $value) {
+ $key = $prohibited ? sprintf('%s_not', $key) : $key;
+
+ switch ($key) {
+ default:
+ throw new FireflyException(sprintf('Cannot handle key "%s" in setObjectDateAfterParams()', $key));
+
+ case 'exact':
+ if ($value instanceof Carbon) {
+ $this->collector->setObjectAfter($value, $field);
+ $this->operators->push(['type' => sprintf('%s_after', $field), 'value' => $value->format('Y-m-d')]);
+ }
+
+ break;
+
+ case 'year':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set date_is_after YEAR value "%s"', $value));
+ $this->collector->objectYearAfter($value, $field);
+ $this->operators->push(['type' => sprintf('%s_after_year', $field), 'value' => $value]);
+ }
+
+ break;
+
+ case 'month':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set date_is_after MONTH value "%s"', $value));
+ $this->collector->objectMonthAfter($value, $field);
+ $this->operators->push(['type' => sprintf('%s_after_month', $field), 'value' => $value]);
+ }
+
+ break;
+
+ case 'day':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set date_is_after DAY value "%s"', $value));
+ $this->collector->objectDayAfter($value, $field);
+ $this->operators->push(['type' => sprintf('%s_after_day', $field), 'value' => $value]);
+ }
+
+ break;
+ }
+ }
+ }
+
+ /**
+ * @throws FireflyException
+ *
+ * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
+ */
+ private function setObjectDateBeforeParams(string $field, array $range, bool $prohibited = false): void
+ {
+ /**
+ * @var string $key
+ * @var Carbon|string $value
+ */
+ foreach ($range as $key => $value) {
+ $key = $prohibited ? sprintf('%s_not', $key) : $key;
+
+ switch ($key) {
+ default:
+ throw new FireflyException(sprintf('Cannot handle key "%s" in setObjectDateBeforeParams()', $key));
+
+ case 'exact':
+ if ($value instanceof Carbon) {
+ $this->collector->setObjectBefore($value, $field);
+ $this->operators->push(['type' => sprintf('%s_before', $field), 'value' => $value->format('Y-m-d')]);
+ }
+
+ break;
+
+ case 'year':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set date_is_before YEAR value "%s"', $value));
+ $this->collector->objectYearBefore($value, $field);
+ $this->operators->push(['type' => sprintf('%s_before_year', $field), 'value' => $value]);
+ }
+
+ break;
+
+ case 'month':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set date_is_before MONTH value "%s"', $value));
+ $this->collector->objectMonthBefore($value, $field);
+ $this->operators->push(['type' => sprintf('%s_before_month', $field), 'value' => $value]);
+ }
+
+ break;
+
+ case 'day':
+ if (is_string($value)) {
+ Log::debug(sprintf('Set date_is_before DAY value "%s"', $value));
+ $this->collector->objectDayBefore($value, $field);
+ $this->operators->push(['type' => sprintf('%s_before_day', $field), 'value' => $value]);
+ }
+
+ break;
+ }
}
}
@@ -448,7 +1420,7 @@ class OperatorQuerySearch implements SearchInterface
break;
case 'source_account_id':
- $account = $this->accountRepository->find((int) $value);
+ $account = $this->accountRepository->find((int)$value);
if (null !== $account) {
$this->collector->setSourceAccounts(new Collection()->push($account));
}
@@ -461,7 +1433,7 @@ class OperatorQuerySearch implements SearchInterface
break;
case '-source_account_id':
- $account = $this->accountRepository->find((int) $value);
+ $account = $this->accountRepository->find((int)$value);
if (null !== $account) {
$this->collector->excludeSourceAccounts(new Collection()->push($account));
}
@@ -578,7 +1550,7 @@ class OperatorQuerySearch implements SearchInterface
break;
case 'destination_account_id':
- $account = $this->accountRepository->find((int) $value);
+ $account = $this->accountRepository->find((int)$value);
if (null !== $account) {
$this->collector->setDestinationAccounts(new Collection()->push($account));
}
@@ -590,7 +1562,7 @@ class OperatorQuerySearch implements SearchInterface
break;
case '-destination_account_id':
- $account = $this->accountRepository->find((int) $value);
+ $account = $this->accountRepository->find((int)$value);
if (null !== $account) {
$this->collector->excludeDestinationAccounts(new Collection()->push($account));
}
@@ -606,7 +1578,7 @@ class OperatorQuerySearch implements SearchInterface
$parts = explode(',', $value);
$collection = new Collection();
foreach ($parts as $accountId) {
- $accountId = (int) $accountId;
+ $accountId = (int)$accountId;
Log::debug(sprintf('Searching for account with ID #%d', $accountId));
$account = $this->accountRepository->find($accountId);
if (null !== $account) {
@@ -632,7 +1604,7 @@ class OperatorQuerySearch implements SearchInterface
$parts = explode(',', $value);
$collection = new Collection();
foreach ($parts as $accountId) {
- $account = $this->accountRepository->find((int) $accountId);
+ $account = $this->accountRepository->find((int)$accountId);
if (null !== $account) {
$collection->push($account);
}
@@ -1947,976 +2919,4 @@ class OperatorQuerySearch implements SearchInterface
return true;
}
-
- /**
- * @throws FireflyException
- */
- public static function getRootOperator(string $operator): string
- {
- $original = $operator;
- // if the string starts with "-" (not), we can remove it and recycle
- // the configuration from the original operator.
- if (str_starts_with($operator, '-')) {
- $operator = substr($operator, 1);
- }
-
- $config = config(sprintf('search.operators.%s', $operator));
- if (null === $config) {
- throw new FireflyException(sprintf('No configuration for search operator "%s"', $operator));
- }
- if (true === $config['alias']) {
- $return = $config['alias_for'];
- if (str_starts_with($original, '-')) {
- $return = sprintf('-%s', $config['alias_for']);
- }
- Log::debug(sprintf('"%s" is an alias for "%s", so return that instead.', $original, $return));
-
- return $return;
- }
- Log::debug(sprintf('"%s" is not an alias.', $operator));
-
- return $original;
- }
-
- /**
- * searchDirection: 1 = source (default), 2 = destination, 3 = both
- * stringPosition: 1 = start (default), 2 = end, 3 = contains, 4 = is
- *
- * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
- * @SuppressWarnings("PHPMD.NPathComplexity")
- */
- private function searchAccount(string $value, SearchDirection $searchDirection, StringPosition $stringPosition, bool $prohibited = false): void
- {
- Log::debug(sprintf('searchAccount("%s", %s, %s)', $value, $stringPosition->name, $searchDirection->name));
-
- // search direction (default): for source accounts
- $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::REVENUE->value];
- $collectorMethod = 'setSourceAccounts';
- if ($prohibited) {
- $collectorMethod = 'excludeSourceAccounts';
- }
-
- // search direction: for destination accounts
- if (SearchDirection::DESTINATION === $searchDirection) { // destination
- // destination can be
- $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value];
- $collectorMethod = 'setDestinationAccounts';
- if ($prohibited) {
- $collectorMethod = 'excludeDestinationAccounts';
- }
- }
- // either account could be:
- if (SearchDirection::BOTH === $searchDirection) {
- $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value, AccountTypeEnum::REVENUE->value];
- $collectorMethod = 'setAccounts';
- if ($prohibited) {
- $collectorMethod = 'excludeAccounts';
- }
- }
- // string position (default): starts with:
- $stringMethod = 'str_starts_with';
-
- // string position: ends with:
- if (StringPosition::ENDS === $stringPosition) {
- $stringMethod = 'str_ends_with';
- }
- if (StringPosition::CONTAINS === $stringPosition) {
- $stringMethod = 'str_contains';
- }
- if (StringPosition::IS === $stringPosition) {
- $stringMethod = 'stringIsEqual';
- }
-
- // get accounts:
- $accounts = $this->accountRepository->searchAccount($value, $searchTypes, 1337);
- if (0 === $accounts->count() && false === $prohibited) {
- Log::warning('Found zero accounts, search for non existing account, NO results will be returned.');
- $this->collector->findNothing();
-
- return;
- }
- if (0 === $accounts->count() && true === $prohibited) {
- Log::debug('Found zero accounts, but the search is negated, so effectively we ignore the search parameter.');
-
- return;
- }
- Log::debug(sprintf('Found %d accounts, will filter.', $accounts->count()));
- $filtered = $accounts->filter(
- static fn (Account $account) => $stringMethod(strtolower($account->name), strtolower($value))
- );
-
- if (0 === $filtered->count()) {
- Log::warning('Left with zero accounts, so cannot find anything, NO results will be returned.');
- $this->collector->findNothing();
-
- return;
- }
- Log::debug(sprintf('Left with %d, set as %s().', $filtered->count(), $collectorMethod));
- $this->collector->{$collectorMethod}($filtered); // @phpstan-ignore-line
- }
-
- /**
- * TODO make enums
- * searchDirection: 1 = source (default), 2 = destination, 3 = both
- * stringPosition: 1 = start (default), 2 = end, 3 = contains, 4 = is
- *
- * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
- * @SuppressWarnings("PHPMD.NPathComplexity")
- */
- private function searchAccountNr(string $value, SearchDirection $searchDirection, StringPosition $stringPosition, bool $prohibited = false): void
- {
- Log::debug(sprintf('searchAccountNr(%s, %d, %d)', $value, $searchDirection->name, $stringPosition->name));
-
- // search direction (default): for source accounts
- $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::REVENUE->value];
- $collectorMethod = 'setSourceAccounts';
- if (true === $prohibited) {
- $collectorMethod = 'excludeSourceAccounts';
- }
-
- // search direction: for destination accounts
- if (SearchDirection::DESTINATION === $searchDirection) {
- // destination can be
- $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value];
- $collectorMethod = 'setDestinationAccounts';
- if (true === $prohibited) {
- $collectorMethod = 'excludeDestinationAccounts';
- }
- }
-
- // either account could be:
- if (SearchDirection::BOTH === $searchDirection) {
- $searchTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::MORTGAGE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::EXPENSE->value, AccountTypeEnum::REVENUE->value];
- $collectorMethod = 'setAccounts';
- if (true === $prohibited) {
- $collectorMethod = 'excludeAccounts';
- }
- }
-
- // string position (default): starts with:
- $stringMethod = 'str_starts_with';
-
- // string position: ends with:
- if (StringPosition::ENDS === $stringPosition) {
- $stringMethod = 'str_ends_with';
- }
- if (StringPosition::CONTAINS === $stringPosition) {
- $stringMethod = 'str_contains';
- }
- if (StringPosition::IS === $stringPosition) {
- $stringMethod = 'stringIsEqual';
- }
-
- // search for accounts:
- $accounts = $this->accountRepository->searchAccountNr($value, $searchTypes, 1337);
- if (0 === $accounts->count()) {
- Log::debug('Found zero accounts, search for invalid account.');
- Log::warning('Call to findNothing() from searchAccountNr().');
- $this->collector->findNothing();
-
- return;
- }
-
- // if found, do filter
- Log::debug(sprintf('Found %d accounts, will filter.', $accounts->count()));
- $filtered = $accounts->filter(
- static function (Account $account) use ($value, $stringMethod) {
- // either IBAN or account number
- $ibanMatch = $stringMethod(strtolower((string) $account->iban), strtolower($value));
- $accountNrMatch = false;
-
- /** @var AccountMeta $meta */
- foreach ($account->accountMeta as $meta) {
- if ('account_number' === $meta->name && $stringMethod(strtolower((string) $meta->data), strtolower($value))) {
- $accountNrMatch = true;
- }
- }
-
- return $ibanMatch || $accountNrMatch;
- }
- );
-
- if (0 === $filtered->count()) {
- Log::debug('Left with zero, search for invalid account');
- Log::warning('Call to findNothing() from searchAccountNr().');
- $this->collector->findNothing();
-
- return;
- }
- Log::debug(sprintf('Left with %d, set as %s().', $filtered->count(), $collectorMethod));
- $this->collector->{$collectorMethod}($filtered); // @phpstan-ignore-line
- }
-
- private function getCashAccount(): Account
- {
- return $this->accountRepository->getCashAccount();
- }
-
- private function findCurrency(string $value): ?TransactionCurrency
- {
- if (str_contains($value, '(') && str_contains($value, ')')) {
- // bad method to split and get the currency code:
- $parts = explode(' ', $value);
- $value = trim($parts[count($parts) - 1], "() \t\n\r\0\x0B");
- }
- $result = $this->currencyRepository->findByCode($value);
- if (null === $result) {
- return $this->currencyRepository->findByName($value);
- }
-
- return $result;
- }
-
- /**
- * @throws FireflyException
- */
- private function parseDateRange(string $type, string $value): array
- {
- $parser = new ParseDateString();
- if ($parser->isDateRange($value)) {
- return $parser->parseRange($value);
- }
-
- try {
- $parsedDate = $parser->parseDate($value);
- } catch (FireflyException) {
- Log::debug(sprintf('Could not parse date "%s", will return empty array.', $value));
- $this->invalidOperators[] = [
- 'type' => $type,
- 'value' => $value,
- ];
-
- return [];
- }
-
- return [
- 'exact' => $parsedDate,
- ];
- }
-
- /**
- * @throws FireflyException
- *
- * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
- */
- private function setExactDateParams(array $range, bool $prohibited = false): void
- {
- /**
- * @var string $key
- * @var Carbon|string $value
- */
- foreach ($range as $key => $value) {
- $key = $prohibited ? sprintf('%s_not', $key) : $key;
-
- switch ($key) {
- default:
- throw new FireflyException(sprintf('Cannot handle key "%s" in setExactParameters()', $key));
-
- case 'exact':
- if ($value instanceof Carbon) {
- Log::debug(sprintf('Set date_is_exact value "%s"', $value->format('Y-m-d')));
- $this->collector->setRange($value, $value);
- $this->operators->push(['type' => 'date_on', 'value' => $value->format('Y-m-d')]);
- }
-
- break;
-
- case 'exact_not':
- if ($value instanceof Carbon) {
- $this->collector->excludeRange($value, $value);
- $this->operators->push(['type' => 'not_date_on', 'value' => $value->format('Y-m-d')]);
- }
-
- break;
-
- case 'year':
- if (is_string($value)) {
- Log::debug(sprintf('Set date_is_exact YEAR value "%s"', $value));
- $this->collector->yearIs($value);
- $this->operators->push(['type' => 'date_on_year', 'value' => $value]);
- }
-
- break;
-
- case 'year_not':
- if (is_string($value)) {
- Log::debug(sprintf('Set date_is_exact_not YEAR value "%s"', $value));
- $this->collector->yearIsNot($value);
- $this->operators->push(['type' => 'not_date_on_year', 'value' => $value]);
- }
-
- break;
-
- case 'month':
- if (is_string($value)) {
- Log::debug(sprintf('Set date_is_exact MONTH value "%s"', $value));
- $this->collector->monthIs($value);
- $this->operators->push(['type' => 'date_on_month', 'value' => $value]);
- }
-
- break;
-
- case 'month_not':
- if (is_string($value)) {
- Log::debug(sprintf('Set date_is_exact not MONTH value "%s"', $value));
- $this->collector->monthIsNot($value);
- $this->operators->push(['type' => 'not_date_on_month', 'value' => $value]);
- }
-
- break;
-
- case 'day':
- if (is_string($value)) {
- Log::debug(sprintf('Set date_is_exact DAY value "%s"', $value));
- $this->collector->dayIs($value);
- $this->operators->push(['type' => 'date_on_day', 'value' => $value]);
- }
-
- break;
-
- case 'day_not':
- if (is_string($value)) {
- Log::debug(sprintf('Set not date_is_exact DAY value "%s"', $value));
- $this->collector->dayIsNot($value);
- $this->operators->push(['type' => 'not_date_on_day', 'value' => $value]);
- }
-
- break;
- }
- }
- }
-
- /**
- * @throws FireflyException
- *
- * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
- */
- private function setDateBeforeParams(array $range, bool $prohibited = false): void
- {
- /**
- * @var string $key
- * @var Carbon|string $value
- */
- foreach ($range as $key => $value) {
- $key = $prohibited ? sprintf('%s_not', $key) : $key;
-
- switch ($key) {
- default:
- throw new FireflyException(sprintf('Cannot handle key "%s" in setDateBeforeParams()', $key));
-
- case 'exact':
- if ($value instanceof Carbon) {
- $this->collector->setBefore($value);
- $this->operators->push(['type' => 'date_before', 'value' => $value->format('Y-m-d')]);
- }
-
- break;
-
- case 'year':
- if (is_string($value)) {
- Log::debug(sprintf('Set date_is_before YEAR value "%s"', $value));
- $this->collector->yearBefore($value);
- $this->operators->push(['type' => 'date_before_year', 'value' => $value]);
- }
-
- break;
-
- case 'month':
- if (is_string($value)) {
- Log::debug(sprintf('Set date_is_before MONTH value "%s"', $value));
- $this->collector->monthBefore($value);
- $this->operators->push(['type' => 'date_before_month', 'value' => $value]);
- }
-
- break;
-
- case 'day':
- if (is_string($value)) {
- Log::debug(sprintf('Set date_is_before DAY value "%s"', $value));
- $this->collector->dayBefore($value);
- $this->operators->push(['type' => 'date_before_day', 'value' => $value]);
- }
-
- break;
- }
- }
- }
-
- /**
- * @throws FireflyException
- *
- * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
- */
- private function setDateAfterParams(array $range, bool $prohibited = false): void
- {
- /**
- * @var string $key
- * @var Carbon|string $value
- */
- foreach ($range as $key => $value) {
- $key = $prohibited ? sprintf('%s_not', $key) : $key;
-
- switch ($key) {
- default:
- throw new FireflyException(sprintf('Cannot handle key "%s" in setDateAfterParams()', $key));
-
- case 'exact':
- if ($value instanceof Carbon) {
- $this->collector->setAfter($value);
- $this->operators->push(['type' => 'date_after', 'value' => $value->format('Y-m-d')]);
- }
-
- break;
-
- case 'year':
- if (is_string($value)) {
- Log::debug(sprintf('Set date_is_after YEAR value "%s"', $value));
- $this->collector->yearAfter($value);
- $this->operators->push(['type' => 'date_after_year', 'value' => $value]);
- }
-
- break;
-
- case 'month':
- if (is_string($value)) {
- Log::debug(sprintf('Set date_is_after MONTH value "%s"', $value));
- $this->collector->monthAfter($value);
- $this->operators->push(['type' => 'date_after_month', 'value' => $value]);
- }
-
- break;
-
- case 'day':
- if (is_string($value)) {
- Log::debug(sprintf('Set date_is_after DAY value "%s"', $value));
- $this->collector->dayAfter($value);
- $this->operators->push(['type' => 'date_after_day', 'value' => $value]);
- }
-
- break;
- }
- }
- }
-
- /**
- * @throws FireflyException
- *
- * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
- */
- private function setExactMetaDateParams(string $field, array $range, bool $prohibited = false): void
- {
- Log::debug('Now in setExactMetaDateParams()');
-
- /**
- * @var string $key
- * @var Carbon|string $value
- */
- foreach ($range as $key => $value) {
- $key = $prohibited ? sprintf('%s_not', $key) : $key;
-
- switch ($key) {
- default:
- throw new FireflyException(sprintf('Cannot handle key "%s" in setExactMetaDateParams()', $key));
-
- case 'exact':
- if ($value instanceof Carbon) {
- Log::debug(sprintf('Set %s_is_exact value "%s"', $field, $value->format('Y-m-d')));
- $this->collector->setMetaDateRange($value, $value, $field);
- $this->operators->push(['type' => sprintf('%s_on', $field), 'value' => $value->format('Y-m-d')]);
- }
-
- break;
-
- case 'exact_not':
- if ($value instanceof Carbon) {
- Log::debug(sprintf('Set NOT %s_is_exact value "%s"', $field, $value->format('Y-m-d')));
- $this->collector->excludeMetaDateRange($value, $value, $field);
- $this->operators->push(['type' => sprintf('not_%s_on', $field), 'value' => $value->format('Y-m-d')]);
- }
-
- break;
-
- case 'year':
- if (is_string($value)) {
- Log::debug(sprintf('Set %s_is_exact YEAR value "%s"', $field, $value));
- $this->collector->metaYearIs($value, $field);
- $this->operators->push(['type' => sprintf('%s_on_year', $field), 'value' => $value]);
- }
-
- break;
-
- case 'year_not':
- if (is_string($value)) {
- Log::debug(sprintf('Set NOT %s_is_exact YEAR value "%s"', $field, $value));
- $this->collector->metaYearIsNot($value, $field);
- $this->operators->push(['type' => sprintf('not_%s_on_year', $field), 'value' => $value]);
- }
-
- break;
-
- case 'month':
- if (is_string($value)) {
- Log::debug(sprintf('Set %s_is_exact MONTH value "%s"', $field, $value));
- $this->collector->metaMonthIs($value, $field);
- $this->operators->push(['type' => sprintf('%s_on_month', $field), 'value' => $value]);
- }
-
- break;
-
- case 'month_not':
- if (is_string($value)) {
- Log::debug(sprintf('Set NOT %s_is_exact MONTH value "%s"', $field, $value));
- $this->collector->metaMonthIsNot($value, $field);
- $this->operators->push(['type' => sprintf('not_%s_on_month', $field), 'value' => $value]);
- }
-
- break;
-
- case 'day':
- if (is_string($value)) {
- Log::debug(sprintf('Set %s_is_exact DAY value "%s"', $field, $value));
- $this->collector->metaDayIs($value, $field);
- $this->operators->push(['type' => sprintf('%s_on_day', $field), 'value' => $value]);
- }
-
- break;
-
- case 'day_not':
- if (is_string($value)) {
- Log::debug(sprintf('Set NOT %s_is_exact DAY value "%s"', $field, $value));
- $this->collector->metaDayIsNot($value, $field);
- $this->operators->push(['type' => sprintf('not_%s_on_day', $field), 'value' => $value]);
- }
-
- break;
- }
- }
- }
-
- /**
- * @throws FireflyException
- *
- * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
- */
- private function setMetaDateBeforeParams(string $field, array $range, bool $prohibited = false): void
- {
- /**
- * @var string $key
- * @var Carbon|string $value
- */
- foreach ($range as $key => $value) {
- $key = $prohibited ? sprintf('%s_not', $key) : $key;
-
- switch ($key) {
- default:
- throw new FireflyException(sprintf('Cannot handle key "%s" in setMetaDateBeforeParams()', $key));
-
- case 'exact':
- if ($value instanceof Carbon) {
- $this->collector->setMetaBefore($value, $field);
- $this->operators->push(['type' => sprintf('%s_before', $field), 'value' => $value->format('Y-m-d')]);
- }
-
- break;
-
- case 'year':
- if (is_string($value)) {
- Log::debug(sprintf('Set %s_is_before YEAR value "%s"', $field, $value));
- $this->collector->metaYearBefore($value, $field);
- $this->operators->push(['type' => sprintf('%s_before_year', $field), 'value' => $value]);
- }
-
- break;
-
- case 'month':
- if (is_string($value)) {
- Log::debug(sprintf('Set %s_is_before MONTH value "%s"', $field, $value));
- $this->collector->metaMonthBefore($value, $field);
- $this->operators->push(['type' => sprintf('%s_before_month', $field), 'value' => $value]);
- }
-
- break;
-
- case 'day':
- if (is_string($value)) {
- Log::debug(sprintf('Set %s_is_before DAY value "%s"', $field, $value));
- $this->collector->metaDayBefore($value, $field);
- $this->operators->push(['type' => sprintf('%s_before_day', $field), 'value' => $value]);
- }
-
- break;
- }
- }
- }
-
- /**
- * @throws FireflyException
- *
- * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
- */
- private function setMetaDateAfterParams(string $field, array $range, bool $prohibited = false): void
- {
- /**
- * @var string $key
- * @var Carbon|string $value
- */
- foreach ($range as $key => $value) {
- $key = $prohibited ? sprintf('%s_not', $key) : $key;
-
- switch ($key) {
- default:
- throw new FireflyException(sprintf('Cannot handle key "%s" in setMetaDateAfterParams()', $key));
-
- case 'exact':
- if ($value instanceof Carbon) {
- $this->collector->setMetaAfter($value, $field);
- $this->operators->push(['type' => sprintf('%s_after', $field), 'value' => $value->format('Y-m-d')]);
- }
-
- break;
-
- case 'year':
- if (is_string($value)) {
- Log::debug(sprintf('Set %s_is_after YEAR value "%s"', $field, $value));
- $this->collector->metaYearAfter($value, $field);
- $this->operators->push(['type' => sprintf('%s_after_year', $field), 'value' => $value]);
- }
-
- break;
-
- case 'month':
- if (is_string($value)) {
- Log::debug(sprintf('Set %s_is_after MONTH value "%s"', $field, $value));
- $this->collector->metaMonthAfter($value, $field);
- $this->operators->push(['type' => sprintf('%s_after_month', $field), 'value' => $value]);
- }
-
- break;
-
- case 'day':
- if (is_string($value)) {
- Log::debug(sprintf('Set %s_is_after DAY value "%s"', $field, $value));
- $this->collector->metaDayAfter($value, $field);
- $this->operators->push(['type' => sprintf('%s_after_day', $field), 'value' => $value]);
- }
-
- break;
- }
- }
- }
-
- /**
- * @throws FireflyException
- *
- * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
- */
- private function setExactObjectDateParams(string $field, array $range, bool $prohibited = false): void
- {
- /**
- * @var string $key
- * @var Carbon|string $value
- */
- foreach ($range as $key => $value) {
- $key = $prohibited ? sprintf('%s_not', $key) : $key;
-
- switch ($key) {
- default:
- throw new FireflyException(sprintf('Cannot handle key "%s" in setExactObjectDateParams()', $key));
-
- case 'exact':
- if ($value instanceof Carbon) {
- Log::debug(sprintf('Set %s_is_exact value "%s"', $field, $value->format('Y-m-d')));
- $this->collector->setObjectRange($value, clone $value, $field);
- $this->operators->push(['type' => sprintf('%s_on', $field), 'value' => $value->format('Y-m-d')]);
- }
-
- break;
-
- case 'exact_not':
- if ($value instanceof Carbon) {
- Log::debug(sprintf('Set NOT %s_is_exact value "%s"', $field, $value->format('Y-m-d')));
- $this->collector->excludeObjectRange($value, clone $value, $field);
- $this->operators->push(['type' => sprintf('not_%s_on', $field), 'value' => $value->format('Y-m-d')]);
- }
-
- break;
-
- case 'year':
- if (is_string($value)) {
- Log::debug(sprintf('Set %s_is_exact YEAR value "%s"', $field, $value));
- $this->collector->objectYearIs($value, $field);
- $this->operators->push(['type' => sprintf('%s_on_year', $field), 'value' => $value]);
- }
-
- break;
-
- case 'year_not':
- if (is_string($value)) {
- Log::debug(sprintf('Set NOT %s_is_exact YEAR value "%s"', $field, $value));
- $this->collector->objectYearIsNot($value, $field);
- $this->operators->push(['type' => sprintf('not_%s_on_year', $field), 'value' => $value]);
- }
-
- break;
-
- case 'month':
- if (is_string($value)) {
- Log::debug(sprintf('Set %s_is_exact MONTH value "%s"', $field, $value));
- $this->collector->objectMonthIs($value, $field);
- $this->operators->push(['type' => sprintf('%s_on_month', $field), 'value' => $value]);
- }
-
- break;
-
- case 'month_not':
- if (is_string($value)) {
- Log::debug(sprintf('Set NOT %s_is_exact MONTH value "%s"', $field, $value));
- $this->collector->objectMonthIsNot($value, $field);
- $this->operators->push(['type' => sprintf('not_%s_on_month', $field), 'value' => $value]);
- }
-
- break;
-
- case 'day':
- if (is_string($value)) {
- Log::debug(sprintf('Set %s_is_exact DAY value "%s"', $field, $value));
- $this->collector->objectDayIs($value, $field);
- $this->operators->push(['type' => sprintf('%s_on_day', $field), 'value' => $value]);
- }
-
- break;
-
- case 'day_not':
- if (is_string($value)) {
- Log::debug(sprintf('Set NOT %s_is_exact DAY value "%s"', $field, $value));
- $this->collector->objectDayIsNot($value, $field);
- $this->operators->push(['type' => sprintf('not_%s_on_day', $field), 'value' => $value]);
- }
-
- break;
- }
- }
- }
-
- /**
- * @throws FireflyException
- *
- * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
- */
- private function setObjectDateBeforeParams(string $field, array $range, bool $prohibited = false): void
- {
- /**
- * @var string $key
- * @var Carbon|string $value
- */
- foreach ($range as $key => $value) {
- $key = $prohibited ? sprintf('%s_not', $key) : $key;
-
- switch ($key) {
- default:
- throw new FireflyException(sprintf('Cannot handle key "%s" in setObjectDateBeforeParams()', $key));
-
- case 'exact':
- if ($value instanceof Carbon) {
- $this->collector->setObjectBefore($value, $field);
- $this->operators->push(['type' => sprintf('%s_before', $field), 'value' => $value->format('Y-m-d')]);
- }
-
- break;
-
- case 'year':
- if (is_string($value)) {
- Log::debug(sprintf('Set date_is_before YEAR value "%s"', $value));
- $this->collector->objectYearBefore($value, $field);
- $this->operators->push(['type' => sprintf('%s_before_year', $field), 'value' => $value]);
- }
-
- break;
-
- case 'month':
- if (is_string($value)) {
- Log::debug(sprintf('Set date_is_before MONTH value "%s"', $value));
- $this->collector->objectMonthBefore($value, $field);
- $this->operators->push(['type' => sprintf('%s_before_month', $field), 'value' => $value]);
- }
-
- break;
-
- case 'day':
- if (is_string($value)) {
- Log::debug(sprintf('Set date_is_before DAY value "%s"', $value));
- $this->collector->objectDayBefore($value, $field);
- $this->operators->push(['type' => sprintf('%s_before_day', $field), 'value' => $value]);
- }
-
- break;
- }
- }
- }
-
- /**
- * @throws FireflyException
- *
- * @SuppressWarnings("PHPMD.BooleanArgumentFlag")
- */
- private function setObjectDateAfterParams(string $field, array $range, bool $prohibited = false): void
- {
- /**
- * @var string $key
- * @var Carbon|string $value
- */
- foreach ($range as $key => $value) {
- $key = $prohibited ? sprintf('%s_not', $key) : $key;
-
- switch ($key) {
- default:
- throw new FireflyException(sprintf('Cannot handle key "%s" in setObjectDateAfterParams()', $key));
-
- case 'exact':
- if ($value instanceof Carbon) {
- $this->collector->setObjectAfter($value, $field);
- $this->operators->push(['type' => sprintf('%s_after', $field), 'value' => $value->format('Y-m-d')]);
- }
-
- break;
-
- case 'year':
- if (is_string($value)) {
- Log::debug(sprintf('Set date_is_after YEAR value "%s"', $value));
- $this->collector->objectYearAfter($value, $field);
- $this->operators->push(['type' => sprintf('%s_after_year', $field), 'value' => $value]);
- }
-
- break;
-
- case 'month':
- if (is_string($value)) {
- Log::debug(sprintf('Set date_is_after MONTH value "%s"', $value));
- $this->collector->objectMonthAfter($value, $field);
- $this->operators->push(['type' => sprintf('%s_after_month', $field), 'value' => $value]);
- }
-
- break;
-
- case 'day':
- if (is_string($value)) {
- Log::debug(sprintf('Set date_is_after DAY value "%s"', $value));
- $this->collector->objectDayAfter($value, $field);
- $this->operators->push(['type' => sprintf('%s_after_day', $field), 'value' => $value]);
- }
-
- break;
- }
- }
- }
-
- private function handleNodeGroup(NodeGroup $node, bool $flipProhibitedFlag): void
- {
- $prohibited = $node->isProhibited($flipProhibitedFlag);
-
- foreach ($node->getNodes() as $subNode) {
- $this->handleSearchNode($subNode, $prohibited);
- }
- }
-
- public function searchTime(): float
- {
- return microtime(true) - $this->startTime;
- }
-
- public function searchTransactions(): LengthAwarePaginator
- {
- $this->parseTagInstructions();
- if (0 === count($this->getWords()) && 0 === count($this->getExcludedWords()) && 0 === count($this->getOperators())) {
- return new LengthAwarePaginator([], 0, 5, 1);
- }
-
- return $this->collector->getPaginatedGroups();
- }
-
- private function parseTagInstructions(): void
- {
- Log::debug('Now in parseTagInstructions()');
- // if exclude tags, remove excluded tags.
- if (count($this->excludeTags) > 0) {
- Log::debug(sprintf('%d exclude tag(s)', count($this->excludeTags)));
- $collection = new Collection();
- foreach ($this->excludeTags as $tagId) {
- $tag = $this->tagRepository->find($tagId);
- if (null !== $tag) {
- Log::debug(sprintf('Exclude tag "%s"', $tag->tag));
- $collection->push($tag);
- }
- }
- Log::debug(sprintf('Selecting all tags except %d excluded tag(s).', $collection->count()));
- $this->collector->setWithoutSpecificTags($collection);
- }
- // if include tags, include them:
- if (count($this->includeTags) > 0) {
- Log::debug(sprintf('%d include tag(s)', count($this->includeTags)));
- $collection = new Collection();
- foreach ($this->includeTags as $tagId) {
- $tag = $this->tagRepository->find($tagId);
- if (null !== $tag) {
- Log::debug(sprintf('Include tag "%s"', $tag->tag));
- $collection->push($tag);
- }
- }
- $this->collector->setAllTags($collection);
- }
- // if include ANY tags, include them: (see #8632)
- if (count($this->includeAnyTags) > 0) {
- Log::debug(sprintf('%d include ANY tag(s)', count($this->includeAnyTags)));
- $collection = new Collection();
- foreach ($this->includeAnyTags as $tagId) {
- $tag = $this->tagRepository->find($tagId);
- if (null !== $tag) {
- Log::debug(sprintf('Include ANY tag "%s"', $tag->tag));
- $collection->push($tag);
- }
- }
- $this->collector->setTags($collection);
- }
- }
-
- public function getWords(): array
- {
- return $this->words;
- }
-
- public function getExcludedWords(): array
- {
- return $this->prohibitedWords;
- }
-
- public function setDate(Carbon $date): void
- {
- $this->date = $date;
- }
-
- public function setPage(int $page): void
- {
- $this->page = $page;
- $this->collector->setPage($this->page);
- }
-
- public function setUser(User $user): void
- {
- $this->accountRepository->setUser($user);
- $this->billRepository->setUser($user);
- $this->categoryRepository->setUser($user);
- $this->budgetRepository->setUser($user);
- $this->tagRepository->setUser($user);
- $this->collector = app(GroupCollectorInterface::class);
- $this->collector->setUser($user);
- $this->collector->withAccountInformation()->withCategoryInformation()->withBudgetInformation();
-
- $this->setLimit((int) app('preferences')->getForUser($user, 'listPageSize', 50)->data);
- }
-
- public function setLimit(int $limit): void
- {
- $this->limit = $limit;
- $this->collector->setLimit($this->limit);
- }
}
diff --git a/app/Support/Search/QueryParser/GdbotsQueryParser.php b/app/Support/Search/QueryParser/GdbotsQueryParser.php
index a402013e48..e532901696 100644
--- a/app/Support/Search/QueryParser/GdbotsQueryParser.php
+++ b/app/Support/Search/QueryParser/GdbotsQueryParser.php
@@ -76,7 +76,7 @@ class GdbotsQueryParser implements QueryParserInterface
case $node instanceof GdbotsNode\Field:
return new FieldNode(
$node->getValue(),
- (string) $node->getNode()->getValue(),
+ (string)$node->getNode()->getValue(),
BoolOperator::PROHIBITED === $node->getBoolOperator()
);
@@ -98,7 +98,7 @@ class GdbotsQueryParser implements QueryParserInterface
case $node instanceof GdbotsNode\Mention:
case $node instanceof GdbotsNode\Emoticon:
case $node instanceof GdbotsNode\Emoji:
- return new StringNode((string) $node->getValue(), BoolOperator::PROHIBITED === $node->getBoolOperator());
+ return new StringNode((string)$node->getValue(), BoolOperator::PROHIBITED === $node->getBoolOperator());
default:
throw new FireflyException(
diff --git a/app/Support/Search/QueryParser/QueryParser.php b/app/Support/Search/QueryParser/QueryParser.php
index c9072f970b..bef27a0ec9 100644
--- a/app/Support/Search/QueryParser/QueryParser.php
+++ b/app/Support/Search/QueryParser/QueryParser.php
@@ -46,22 +46,6 @@ class QueryParser implements QueryParserInterface
return $this->buildNodeGroup(false);
}
- private function buildNodeGroup(bool $isSubquery, bool $prohibited = false): NodeGroup
- {
- $nodes = [];
- $nodeResult = $this->buildNextNode($isSubquery);
-
- while ($nodeResult->node instanceof Node) {
- $nodes[] = $nodeResult->node;
- if ($nodeResult->isSubqueryEnd) {
- break;
- }
- $nodeResult = $this->buildNextNode($isSubquery);
- }
-
- return new NodeGroup($nodes, $prohibited);
- }
-
private function buildNextNode(bool $isSubquery): NodeResult
{
$tokenUnderConstruction = '';
@@ -194,6 +178,22 @@ class QueryParser implements QueryParserInterface
return new NodeResult($finalNode, true);
}
+ private function buildNodeGroup(bool $isSubquery, bool $prohibited = false): NodeGroup
+ {
+ $nodes = [];
+ $nodeResult = $this->buildNextNode($isSubquery);
+
+ while ($nodeResult->node instanceof Node) {
+ $nodes[] = $nodeResult->node;
+ if ($nodeResult->isSubqueryEnd) {
+ break;
+ }
+ $nodeResult = $this->buildNextNode($isSubquery);
+ }
+
+ return new NodeGroup($nodes, $prohibited);
+ }
+
private function createNode(string $token, string $fieldName, bool $prohibited): Node
{
if ('' !== $fieldName) {
diff --git a/app/Support/Singleton/PreferencesSingleton.php b/app/Support/Singleton/PreferencesSingleton.php
index 32b9bb94f6..646e1c60bf 100644
--- a/app/Support/Singleton/PreferencesSingleton.php
+++ b/app/Support/Singleton/PreferencesSingleton.php
@@ -38,13 +38,18 @@ class PreferencesSingleton
public static function getInstance(): self
{
- if (null === self::$instance) {
+ if (!self::$instance instanceof self) {
self::$instance = new self();
}
return self::$instance;
}
+ public function getPreference(string $key): mixed
+ {
+ return $this->preferences[$key] ?? null;
+ }
+
public function resetPreferences(): void
{
$this->preferences = [];
@@ -54,9 +59,4 @@ class PreferencesSingleton
{
$this->preferences[$key] = $value;
}
-
- public function getPreference(string $key): mixed
- {
- return $this->preferences[$key] ?? null;
- }
}
diff --git a/app/Support/Steam.php b/app/Support/Steam.php
index e103f19ece..71d5e788f3 100644
--- a/app/Support/Steam.php
+++ b/app/Support/Steam.php
@@ -47,6 +47,82 @@ use function Safe\preg_replace;
*/
class Steam
{
+ public function accountsBalancesOptimized(Collection $accounts, Carbon $date, ?TransactionCurrency $primary = null, ?bool $convertToPrimary = null): array
+ {
+ Log::debug(sprintf('accountsBalancesOptimized: Called for %d account(s) with date/time "%s"', $accounts->count(), $date->toIso8601String()));
+ $result = [];
+ $convertToPrimary ??= Amount::convertToPrimary();
+ $primary ??= Amount::getPrimaryCurrency();
+ $currencies = $this->getCurrencies($accounts);
+
+ // balance(s) in all currencies for ALL accounts.
+ $arrayOfSums = Transaction::whereIn('account_id', $accounts->pluck('id')->toArray())
+ ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
+ ->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id')
+ ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s'))
+ ->groupBy(['transactions.account_id', 'transaction_currencies.code'])
+ ->get(['transactions.account_id', 'transaction_currencies.code', DB::raw('SUM(transactions.amount) as sum_of_amount')])->toArray()
+ ;
+
+ /** @var Account $account */
+ foreach ($accounts as $account) {
+ // this array is PER account, so we wait a bit before we change code here.
+ $return = [
+ 'pc_balance' => '0',
+ 'balance' => '0', // this key is overwritten right away, but I must remember it is always created.
+ ];
+ $currency = $currencies[$account->id];
+
+ // second array
+ $accountSum = array_filter($arrayOfSums, fn ($entry) => $entry['account_id'] === $account->id);
+ if (0 === count($accountSum)) {
+ $result[$account->id] = $return;
+
+ continue;
+ }
+ $accountSum = array_values($accountSum)[0];
+ $sumOfAmount = (string)$accountSum['sum_of_amount'];
+ $sumOfAmount = $this->floatalize('' === $sumOfAmount ? '0' : $sumOfAmount);
+ $sumsByCode = [
+ $accountSum['code'] => $sumOfAmount,
+ ];
+
+ // Log::debug('All balances are (joined)', $others);
+ // if there is no request to convert, take this as "balance" and "pc_balance".
+ $return['balance'] = $sumsByCode[$currency->code] ?? '0';
+ if (!$convertToPrimary) {
+ unset($return['pc_balance']);
+ // Log::debug(sprintf('Set balance to %s, unset pc_balance', $return['balance']));
+ }
+ // if there is a request to convert, convert to "pc_balance" and use "balance" for whichever amount is in the primary currency.
+ if ($convertToPrimary) {
+ $return['pc_balance'] = $this->convertAllBalances($sumsByCode, $primary, $date);
+ // Log::debug(sprintf('Set pc_balance to %s', $return['pc_balance']));
+ }
+
+ // either way, the balance is always combined with the virtual balance:
+ $virtualBalance = (string)('' === (string)$account->virtual_balance ? '0' : $account->virtual_balance);
+
+ if ($convertToPrimary) {
+ // the primary currency balance is combined with a converted virtual_balance:
+ $converter = new ExchangeRateConverter();
+ $pcVirtualBalance = $converter->convert($currency, $primary, $date, $virtualBalance);
+ $return['pc_balance'] = bcadd($pcVirtualBalance, $return['pc_balance']);
+ // Log::debug(sprintf('Primary virtual balance makes the primary total %s', $return['pc_balance']));
+ }
+ if (!$convertToPrimary) {
+ // if not, also increase the balance + primary balance for consistency.
+ $return['balance'] = bcadd($return['balance'], $virtualBalance);
+ // Log::debug(sprintf('Virtual balance makes the (primary currency) total %s', $return['balance']));
+ }
+ $final = array_merge($return, $sumsByCode);
+ $result[$account->id] = $final;
+ // Log::debug('Final balance is', $final);
+ }
+
+ return $result;
+ }
+
/**
* https://stackoverflow.com/questions/1642614/how-to-ceil-floor-and-round-bcmath-numbers
*/
@@ -75,18 +151,6 @@ class Steam
return $number;
}
- public function filterAccountBalances(array $total, Account $account, bool $convertToPrimary, ?TransactionCurrency $currency = null): array
- {
- Log::debug(sprintf('filterAccountBalances(#%d)', $account->id));
- $return = [];
- foreach ($total as $key => $value) {
- $return[$key] = $this->filterAccountBalance($value, $account, $convertToPrimary, $currency);
- }
- Log::debug(sprintf('end of filterAccountBalances(#%d)', $account->id));
-
- return $return;
- }
-
public function filterAccountBalance(array $set, Account $account, bool $convertToPrimary, ?TransactionCurrency $currency = null): array
{
Log::debug(sprintf('filterAccountBalance(#%d)', $account->id), $set);
@@ -138,6 +202,18 @@ class Steam
return $set;
}
+ public function filterAccountBalances(array $total, Account $account, bool $convertToPrimary, ?TransactionCurrency $currency = null): array
+ {
+ Log::debug(sprintf('filterAccountBalances(#%d)', $account->id));
+ $return = [];
+ foreach ($total as $key => $value) {
+ $return[$key] = $this->filterAccountBalance($value, $account, $convertToPrimary, $currency);
+ }
+ Log::debug(sprintf('end of filterAccountBalances(#%d)', $account->id));
+
+ return $return;
+ }
+
public function filterSpaces(string $string): string
{
$search = [
@@ -197,6 +273,95 @@ class Steam
return str_replace($search, '', $string);
}
+ /**
+ * Returns smaller than or equal to, so be careful with END OF DAY.
+ *
+ * Returns the balance of an account at exact moment given. Array with at least one value.
+ * Always returns:
+ * "balance": balance in the account's currency OR user's primary currency if the account has no currency
+ * "EUR": balance in EUR (or whatever currencies the account has balance in)
+ *
+ * If the user has $convertToPrimary:
+ * "balance": balance in the account's currency OR user's primary currency if the account has no currency
+ * --> "pc_balance": balance in the user's primary currency, with all amounts converted to the primary currency.
+ * "EUR": balance in EUR (or whatever currencies the account has balance in)
+ */
+ public function finalAccountBalance(Account $account, Carbon $date, ?TransactionCurrency $primary = null, ?bool $convertToPrimary = null): array
+ {
+
+ $cache = new CacheProperties();
+ $cache->addProperty($account->id);
+ $cache->addProperty($date);
+ if ($cache->has()) {
+ Log::debug(sprintf('CACHED finalAccountBalance(#%d, %s)', $account->id, $date->format('Y-m-d H:i:s')));
+
+ // return $cache->get();
+ }
+ // Log::debug(sprintf('finalAccountBalance(#%d, %s)', $account->id, $date->format('Y-m-d H:i:s')));
+ if (null === $convertToPrimary) {
+ $convertToPrimary = Amount::convertToPrimary($account->user);
+ }
+ if (!$primary instanceof TransactionCurrency) {
+ $primary = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup);
+ }
+ // account balance thing.
+ $currencyPresent = isset($account->meta) && array_key_exists('currency', $account->meta) && null !== $account->meta['currency'];
+ if ($currencyPresent) {
+ $accountCurrency = $account->meta['currency'];
+ }
+ if (!$currencyPresent) {
+
+ $accountCurrency = $this->getAccountCurrency($account);
+ }
+ $hasCurrency = null !== $accountCurrency;
+ $currency = $hasCurrency ? $accountCurrency : $primary;
+ $return = [
+ 'pc_balance' => '0',
+ 'balance' => '0', // this key is overwritten right away, but I must remember it is always created.
+ ];
+ // balance(s) in all currencies.
+ $array = $account->transactions()
+ ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
+ ->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id')
+ ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s'))
+ ->get(['transaction_currencies.code', 'transactions.amount'])->toArray()
+ ;
+ $others = $this->groupAndSumTransactions($array, 'code', 'amount');
+ // Log::debug('All balances are (joined)', $others);
+ // if there is no request to convert, take this as "balance" and "pc_balance".
+ $return['balance'] = $others[$currency->code] ?? '0';
+ if (!$convertToPrimary) {
+ unset($return['pc_balance']);
+ // Log::debug(sprintf('Set balance to %s, unset pc_balance', $return['balance']));
+ }
+ // if there is a request to convert, convert to "pc_balance" and use "balance" for whichever amount is in the primary currency.
+ if ($convertToPrimary) {
+ $return['pc_balance'] = $this->convertAllBalances($others, $primary, $date); // todo sum all and convert.
+ // Log::debug(sprintf('Set pc_balance to %s', $return['pc_balance']));
+ }
+
+ // either way, the balance is always combined with the virtual balance:
+ $virtualBalance = (string)('' === (string)$account->virtual_balance ? '0' : $account->virtual_balance);
+
+ if ($convertToPrimary) {
+ // the primary currency balance is combined with a converted virtual_balance:
+ $converter = new ExchangeRateConverter();
+ $pcVirtualBalance = $converter->convert($currency, $primary, $date, $virtualBalance);
+ $return['pc_balance'] = bcadd($pcVirtualBalance, $return['pc_balance']);
+ // Log::debug(sprintf('Primary virtual balance makes the primary total %s', $return['pc_balance']));
+ }
+ if (!$convertToPrimary) {
+ // if not, also increase the balance + primary balance for consistency.
+ $return['balance'] = bcadd($return['balance'], $virtualBalance);
+ // Log::debug(sprintf('Virtual balance makes the (primary currency) total %s', $return['balance']));
+ }
+ $final = array_merge($return, $others);
+ // Log::debug('Final balance is', $final);
+ $cache->store($final);
+
+ return $final;
+ }
+
public function finalAccountBalanceInRange(Account $account, Carbon $start, Carbon $end, bool $convertToPrimary): array
{
// expand period.
@@ -321,169 +486,34 @@ class Steam
return $balances;
}
- public function accountsBalancesOptimized(Collection $accounts, Carbon $date, ?TransactionCurrency $primary = null, ?bool $convertToPrimary = null): array
- {
- Log::debug(sprintf('accountsBalancesOptimized: Called for %d account(s) with date/time "%s"', $accounts->count(), $date->toIso8601String()));
- $result = [];
- $convertToPrimary ??= Amount::convertToPrimary();
- $primary ??= Amount::getPrimaryCurrency();
- $currencies = $this->getCurrencies($accounts);
-
- // balance(s) in all currencies for ALL accounts.
- $arrayOfSums = Transaction::whereIn('account_id', $accounts->pluck('id')->toArray())
- ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
- ->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id')
- ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s'))
- ->groupBy(['transactions.account_id', 'transaction_currencies.code'])
- ->get(['transactions.account_id', 'transaction_currencies.code', DB::raw('SUM(transactions.amount) as sum_of_amount')])->toArray()
- ;
-
- /** @var Account $account */
- foreach ($accounts as $account) {
- // this array is PER account, so we wait a bit before we change code here.
- $return = [
- 'pc_balance' => '0',
- 'balance' => '0', // this key is overwritten right away, but I must remember it is always created.
- ];
- $currency = $currencies[$account->id];
-
- // second array
- $accountSum = array_filter($arrayOfSums, fn ($entry) => $entry['account_id'] === $account->id);
- if (0 === count($accountSum)) {
- $result[$account->id] = $return;
-
- continue;
- }
- $accountSum = array_values($accountSum)[0];
- $sumOfAmount = (string)$accountSum['sum_of_amount'];
- $sumOfAmount = $this->floatalize('' === $sumOfAmount ? '0' : $sumOfAmount);
- $sumsByCode = [
- $accountSum['code'] => $sumOfAmount,
- ];
-
- // Log::debug('All balances are (joined)', $others);
- // if there is no request to convert, take this as "balance" and "pc_balance".
- $return['balance'] = $sumsByCode[$currency->code] ?? '0';
- if (!$convertToPrimary) {
- unset($return['pc_balance']);
- // Log::debug(sprintf('Set balance to %s, unset pc_balance', $return['balance']));
- }
- // if there is a request to convert, convert to "pc_balance" and use "balance" for whichever amount is in the primary currency.
- if ($convertToPrimary) {
- $return['pc_balance'] = $this->convertAllBalances($sumsByCode, $primary, $date);
- // Log::debug(sprintf('Set pc_balance to %s', $return['pc_balance']));
- }
-
- // either way, the balance is always combined with the virtual balance:
- $virtualBalance = (string)('' === (string)$account->virtual_balance ? '0' : $account->virtual_balance);
-
- if ($convertToPrimary) {
- // the primary currency balance is combined with a converted virtual_balance:
- $converter = new ExchangeRateConverter();
- $pcVirtualBalance = $converter->convert($currency, $primary, $date, $virtualBalance);
- $return['pc_balance'] = bcadd($pcVirtualBalance, $return['pc_balance']);
- // Log::debug(sprintf('Primary virtual balance makes the primary total %s', $return['pc_balance']));
- }
- if (!$convertToPrimary) {
- // if not, also increase the balance + primary balance for consistency.
- $return['balance'] = bcadd($return['balance'], $virtualBalance);
- // Log::debug(sprintf('Virtual balance makes the (primary currency) total %s', $return['balance']));
- }
- $final = array_merge($return, $sumsByCode);
- $result[$account->id] = $final;
- // Log::debug('Final balance is', $final);
- }
-
- return $result;
- }
-
/**
- * Returns smaller than or equal to, so be careful with END OF DAY.
+ * https://framework.zend.com/downloads/archives
*
- * Returns the balance of an account at exact moment given. Array with at least one value.
- * Always returns:
- * "balance": balance in the account's currency OR user's primary currency if the account has no currency
- * "EUR": balance in EUR (or whatever currencies the account has balance in)
- *
- * If the user has $convertToPrimary:
- * "balance": balance in the account's currency OR user's primary currency if the account has no currency
- * --> "pc_balance": balance in the user's primary currency, with all amounts converted to the primary currency.
- * "EUR": balance in EUR (or whatever currencies the account has balance in)
+ * Convert a scientific notation to float
+ * Additionally fixed a problem with PHP <= 5.2.x with big integers
*/
- public function finalAccountBalance(Account $account, Carbon $date, ?TransactionCurrency $primary = null, ?bool $convertToPrimary = null): array
+ public function floatalize(string $value): string
{
+ $value = strtoupper($value);
+ if (!str_contains($value, 'E')) {
+ return $value;
+ }
+ Log::debug(sprintf('Floatalizing %s', $value));
- $cache = new CacheProperties();
- $cache->addProperty($account->id);
- $cache->addProperty($date);
- if ($cache->has()) {
- Log::debug(sprintf('CACHED finalAccountBalance(#%d, %s)', $account->id, $date->format('Y-m-d H:i:s')));
+ $number = substr($value, 0, (int)strpos($value, 'E'));
+ if (str_contains($number, '.')) {
+ $post = strlen(substr($number, (int)strpos($number, '.') + 1));
+ $mantis = substr($value, (int)strpos($value, 'E') + 1);
+ if ($mantis < 0) {
+ $post += abs((int)$mantis);
+ }
- // return $cache->get();
- }
- // Log::debug(sprintf('finalAccountBalance(#%d, %s)', $account->id, $date->format('Y-m-d H:i:s')));
- if (null === $convertToPrimary) {
- $convertToPrimary = Amount::convertToPrimary($account->user);
- }
- if (!$primary instanceof TransactionCurrency) {
- $primary = Amount::getPrimaryCurrencyByUserGroup($account->user->userGroup);
- }
- // account balance thing.
- $currencyPresent = isset($account->meta) && array_key_exists('currency', $account->meta) && null !== $account->meta['currency'];
- if ($currencyPresent) {
- $accountCurrency = $account->meta['currency'];
- }
- if (!$currencyPresent) {
-
- $accountCurrency = $this->getAccountCurrency($account);
- }
- $hasCurrency = null !== $accountCurrency;
- $currency = $hasCurrency ? $accountCurrency : $primary;
- $return = [
- 'pc_balance' => '0',
- 'balance' => '0', // this key is overwritten right away, but I must remember it is always created.
- ];
- // balance(s) in all currencies.
- $array = $account->transactions()
- ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
- ->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transactions.transaction_currency_id')
- ->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s'))
- ->get(['transaction_currencies.code', 'transactions.amount'])->toArray()
- ;
- $others = $this->groupAndSumTransactions($array, 'code', 'amount');
- // Log::debug('All balances are (joined)', $others);
- // if there is no request to convert, take this as "balance" and "pc_balance".
- $return['balance'] = $others[$currency->code] ?? '0';
- if (!$convertToPrimary) {
- unset($return['pc_balance']);
- // Log::debug(sprintf('Set balance to %s, unset pc_balance', $return['balance']));
- }
- // if there is a request to convert, convert to "pc_balance" and use "balance" for whichever amount is in the primary currency.
- if ($convertToPrimary) {
- $return['pc_balance'] = $this->convertAllBalances($others, $primary, $date); // todo sum all and convert.
- // Log::debug(sprintf('Set pc_balance to %s', $return['pc_balance']));
+ // TODO careless float could break financial math.
+ return number_format((float)$value, $post, '.', '');
}
- // either way, the balance is always combined with the virtual balance:
- $virtualBalance = (string)('' === (string)$account->virtual_balance ? '0' : $account->virtual_balance);
-
- if ($convertToPrimary) {
- // the primary currency balance is combined with a converted virtual_balance:
- $converter = new ExchangeRateConverter();
- $pcVirtualBalance = $converter->convert($currency, $primary, $date, $virtualBalance);
- $return['pc_balance'] = bcadd($pcVirtualBalance, $return['pc_balance']);
- // Log::debug(sprintf('Primary virtual balance makes the primary total %s', $return['pc_balance']));
- }
- if (!$convertToPrimary) {
- // if not, also increase the balance + primary balance for consistency.
- $return['balance'] = bcadd($return['balance'], $virtualBalance);
- // Log::debug(sprintf('Virtual balance makes the (primary currency) total %s', $return['balance']));
- }
- $final = array_merge($return, $others);
- // Log::debug('Final balance is', $final);
- $cache->store($final);
-
- return $final;
+ // TODO careless float could break financial math.
+ return number_format((float)$value, 0, '.', '');
}
public function getAccountCurrency(Account $account): ?TransactionCurrency
@@ -503,45 +533,6 @@ class Steam
return Amount::getTransactionCurrencyById((int)$result->data);
}
- private function groupAndSumTransactions(array $array, string $group, string $field): array
- {
- $return = [];
-
- foreach ($array as $item) {
- $groupKey = $item[$group] ?? 'unknown';
- $return[$groupKey] = bcadd($return[$groupKey] ?? '0', (string)$item[$field]);
- }
-
- return $return;
- }
-
- private function convertAllBalances(array $others, TransactionCurrency $primary, Carbon $date): string
- {
- $total = '0';
- $converter = new ExchangeRateConverter();
- $singleton = PreferencesSingleton::getInstance();
- foreach ($others as $key => $amount) {
- $preference = $singleton->getPreference($key);
-
- try {
- $currency = $preference ?? Amount::getTransactionCurrencyByCode($key);
- } catch (FireflyException) {
- continue;
- }
- if (null === $preference) {
- $singleton->setPreference($key, $currency);
- }
- $current = $amount;
- if ($currency->id !== $primary->id) {
- $current = $converter->convert($currency, $primary, $date, $amount);
- Log::debug(sprintf('Convert %s %s to %s %s', $currency->code, $amount, $primary->code, $current));
- }
- $total = bcadd($current, $total);
- }
-
- return $total;
- }
-
/**
* @throws FireflyException
*/
@@ -563,6 +554,21 @@ class Steam
return (string)$host;
}
+ /**
+ * Get user's language.
+ *
+ * @throws FireflyException
+ */
+ public function getLanguage(): string // get preference
+ {
+ $preference = app('preferences')->get('language', config('firefly.default_language', 'en_US'))->data;
+ if (!is_string($preference)) {
+ throw new FireflyException(sprintf('Preference "language" must be a string, but is unexpectedly a "%s".', gettype($preference)));
+ }
+
+ return str_replace('-', '_', $preference);
+ }
+
public function getLastActivities(array $accounts): array
{
$list = [];
@@ -588,38 +594,29 @@ class Steam
*/
public function getLocale(): string // get preference
{
- $locale = app('preferences')->get('locale', config('firefly.default_locale', 'equal'))->data;
+ $singleton = PreferencesSingleton::getInstance();
+ $cached = $singleton->getPreference('locale');
+ if (null !== $cached) {
+ return $cached;
+ }
+ $locale = app('preferences')->get('locale', config('firefly.default_locale', 'equal'))->data;
if (is_array($locale)) {
$locale = 'equal';
}
if ('equal' === $locale) {
$locale = $this->getLanguage();
}
- $locale = (string)$locale;
+ $locale = (string)$locale;
// Check for Windows to replace the locale correctly.
if ('WIN' === strtoupper(substr(PHP_OS, 0, 3))) {
- return str_replace('_', '-', $locale);
+ $locale = str_replace('_', '-', $locale);
}
+ $singleton->setPreference('locale', $locale);
return $locale;
}
- /**
- * Get user's language.
- *
- * @throws FireflyException
- */
- public function getLanguage(): string // get preference
- {
- $preference = app('preferences')->get('language', config('firefly.default_language', 'en_US'))->data;
- if (!is_string($preference)) {
- throw new FireflyException(sprintf('Preference "language" must be a string, but is unexpectedly a "%s".', gettype($preference)));
- }
-
- return str_replace('-', '_', $preference);
- }
-
public function getLocaleArray(string $locale): array
{
return [
@@ -681,36 +678,6 @@ class Steam
return $amount;
}
- /**
- * https://framework.zend.com/downloads/archives
- *
- * Convert a scientific notation to float
- * Additionally fixed a problem with PHP <= 5.2.x with big integers
- */
- public function floatalize(string $value): string
- {
- $value = strtoupper($value);
- if (!str_contains($value, 'E')) {
- return $value;
- }
- Log::debug(sprintf('Floatalizing %s', $value));
-
- $number = substr($value, 0, (int)strpos($value, 'E'));
- if (str_contains($number, '.')) {
- $post = strlen(substr($number, (int)strpos($number, '.') + 1));
- $mantis = substr($value, (int)strpos($value, 'E') + 1);
- if ($mantis < 0) {
- $post += abs((int)$mantis);
- }
-
- // TODO careless float could break financial math.
- return number_format((float)$value, $post, '.', '');
- }
-
- // TODO careless float could break financial math.
- return number_format((float)$value, 0, '.', '');
- }
-
public function opposite(?string $amount = null): ?string
{
if (null === $amount) {
@@ -768,6 +735,33 @@ class Steam
return $amount;
}
+ private function convertAllBalances(array $others, TransactionCurrency $primary, Carbon $date): string
+ {
+ $total = '0';
+ $converter = new ExchangeRateConverter();
+ $singleton = PreferencesSingleton::getInstance();
+ foreach ($others as $key => $amount) {
+ $preference = $singleton->getPreference($key);
+
+ try {
+ $currency = $preference ?? Amount::getTransactionCurrencyByCode($key);
+ } catch (FireflyException) {
+ continue;
+ }
+ if (null === $preference) {
+ $singleton->setPreference($key, $currency);
+ }
+ $current = $amount;
+ if ($currency->id !== $primary->id) {
+ $current = $converter->convert($currency, $primary, $date, $amount);
+ Log::debug(sprintf('Convert %s %s to %s %s', $currency->code, $amount, $primary->code, $current));
+ }
+ $total = bcadd((string) $current, $total);
+ }
+
+ return $total;
+ }
+
private function getCurrencies(Collection $accounts): array
{
$currencies = [];
@@ -811,4 +805,16 @@ class Steam
return $accountCurrencies;
}
+
+ private function groupAndSumTransactions(array $array, string $group, string $field): array
+ {
+ $return = [];
+
+ foreach ($array as $item) {
+ $groupKey = $item[$group] ?? 'unknown';
+ $return[$groupKey] = bcadd($return[$groupKey] ?? '0', (string)$item[$field]);
+ }
+
+ return $return;
+ }
}
diff --git a/app/Support/System/GeneratesInstallationId.php b/app/Support/System/GeneratesInstallationId.php
index 20cd0a303c..732237214f 100644
--- a/app/Support/System/GeneratesInstallationId.php
+++ b/app/Support/System/GeneratesInstallationId.php
@@ -49,7 +49,7 @@ trait GeneratesInstallationId
if (null === $config) {
$uuid4 = Uuid::uuid4();
- $uniqueId = (string) $uuid4;
+ $uniqueId = (string)$uuid4;
app('log')->info(sprintf('Created Firefly III installation ID %s', $uniqueId));
app('fireflyconfig')->set('installation_id', $uniqueId);
}
diff --git a/app/Support/System/OAuthKeys.php b/app/Support/System/OAuthKeys.php
index 53e353481f..97bd74bd19 100644
--- a/app/Support/System/OAuthKeys.php
+++ b/app/Support/System/OAuthKeys.php
@@ -43,22 +43,18 @@ class OAuthKeys
private const string PRIVATE_KEY = 'oauth_private_key';
private const string PUBLIC_KEY = 'oauth_public_key';
- public static function verifyKeysRoutine(): void
+ public static function generateKeys(): void
{
- if (!self::keysInDatabase() && !self::hasKeyFiles()) {
- self::generateKeys();
- self::storeKeysInDB();
+ Artisan::registerCommand(new KeysCommand());
+ Artisan::call('firefly-iii:laravel-passport-keys');
+ }
- return;
- }
- if (self::keysInDatabase() && !self::hasKeyFiles()) {
- self::restoreKeysFromDB();
+ public static function hasKeyFiles(): bool
+ {
+ $private = storage_path('oauth-private.key');
+ $public = storage_path('oauth-public.key');
- return;
- }
- if (!self::keysInDatabase() && self::hasKeyFiles()) {
- self::storeKeysInDB();
- }
+ return file_exists($private) && file_exists($public);
}
public static function keysInDatabase(): bool
@@ -68,8 +64,8 @@ class OAuthKeys
// better check if keys are in the database:
if (app('fireflyconfig')->has(self::PRIVATE_KEY) && app('fireflyconfig')->has(self::PUBLIC_KEY)) {
try {
- $privateKey = (string) app('fireflyconfig')->get(self::PRIVATE_KEY)?->data;
- $publicKey = (string) app('fireflyconfig')->get(self::PUBLIC_KEY)?->data;
+ $privateKey = (string)app('fireflyconfig')->get(self::PRIVATE_KEY)?->data;
+ $publicKey = (string)app('fireflyconfig')->get(self::PUBLIC_KEY)?->data;
} catch (ContainerExceptionInterface|FireflyException|NotFoundExceptionInterface $e) {
app('log')->error(sprintf('Could not validate keysInDatabase(): %s', $e->getMessage()));
app('log')->error($e->getTraceAsString());
@@ -82,35 +78,13 @@ class OAuthKeys
return false;
}
- public static function hasKeyFiles(): bool
- {
- $private = storage_path('oauth-private.key');
- $public = storage_path('oauth-public.key');
-
- return file_exists($private) && file_exists($public);
- }
-
- public static function generateKeys(): void
- {
- Artisan::registerCommand(new KeysCommand());
- Artisan::call('firefly-iii:laravel-passport-keys');
- }
-
- public static function storeKeysInDB(): void
- {
- $private = storage_path('oauth-private.key');
- $public = storage_path('oauth-public.key');
- app('fireflyconfig')->set(self::PRIVATE_KEY, Crypt::encrypt(file_get_contents($private)));
- app('fireflyconfig')->set(self::PUBLIC_KEY, Crypt::encrypt(file_get_contents($public)));
- }
-
/**
* @throws FireflyException
*/
public static function restoreKeysFromDB(): bool
{
- $privateKey = (string) app('fireflyconfig')->get(self::PRIVATE_KEY)?->data;
- $publicKey = (string) app('fireflyconfig')->get(self::PUBLIC_KEY)?->data;
+ $privateKey = (string)app('fireflyconfig')->get(self::PRIVATE_KEY)?->data;
+ $publicKey = (string)app('fireflyconfig')->get(self::PUBLIC_KEY)?->data;
try {
$privateContent = Crypt::decrypt($privateKey);
@@ -132,4 +106,30 @@ class OAuthKeys
return true;
}
+
+ public static function storeKeysInDB(): void
+ {
+ $private = storage_path('oauth-private.key');
+ $public = storage_path('oauth-public.key');
+ app('fireflyconfig')->set(self::PRIVATE_KEY, Crypt::encrypt(file_get_contents($private)));
+ app('fireflyconfig')->set(self::PUBLIC_KEY, Crypt::encrypt(file_get_contents($public)));
+ }
+
+ public static function verifyKeysRoutine(): void
+ {
+ if (!self::keysInDatabase() && !self::hasKeyFiles()) {
+ self::generateKeys();
+ self::storeKeysInDB();
+
+ return;
+ }
+ if (self::keysInDatabase() && !self::hasKeyFiles()) {
+ self::restoreKeysFromDB();
+
+ return;
+ }
+ if (!self::keysInDatabase() && self::hasKeyFiles()) {
+ self::storeKeysInDB();
+ }
+ }
}
diff --git a/app/Support/Twig/AmountFormat.php b/app/Support/Twig/AmountFormat.php
index ac6efaa8b7..dbf22254b3 100644
--- a/app/Support/Twig/AmountFormat.php
+++ b/app/Support/Twig/AmountFormat.php
@@ -29,10 +29,10 @@ use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Support\Facades\Amount;
use Illuminate\Support\Facades\Log;
+use Override;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
-use Override;
/**
* Contains all amount formatting routines.
@@ -48,6 +48,17 @@ class AmountFormat extends AbstractExtension
];
}
+ #[Override]
+ public function getFunctions(): array
+ {
+ return [
+ $this->formatAmountByAccount(),
+ $this->formatAmountBySymbol(),
+ $this->formatAmountByCurrency(),
+ $this->formatAmountByCode(),
+ ];
+ }
+
protected function formatAmount(): TwigFilter
{
return new TwigFilter(
@@ -61,30 +72,6 @@ class AmountFormat extends AbstractExtension
);
}
- protected function formatAmountPlain(): TwigFilter
- {
- return new TwigFilter(
- 'formatAmountPlain',
- static function (string $string): string {
- $currency = Amount::getPrimaryCurrency();
-
- return Amount::formatAnything($currency, $string, false);
- },
- ['is_safe' => ['html']]
- );
- }
-
- #[Override]
- public function getFunctions(): array
- {
- return [
- $this->formatAmountByAccount(),
- $this->formatAmountBySymbol(),
- $this->formatAmountByCurrency(),
- $this->formatAmountByCode(),
- ];
- }
-
/**
* Will format the amount by the currency related to the given account.
*
@@ -107,50 +94,6 @@ class AmountFormat extends AbstractExtension
);
}
- /**
- * Will format the amount by the currency related to the given account.
- */
- protected function formatAmountBySymbol(): TwigFunction
- {
- return new TwigFunction(
- 'formatAmountBySymbol',
- static function (string $amount, ?string $symbol, ?int $decimalPlaces = null, ?bool $coloured = null): string {
-
- if (null === $symbol) {
- $message = sprintf('formatAmountBySymbol("%s", %s, %d, %s) was called without a symbol. Please browse to /flush to clear your cache.', $amount, var_export($symbol, true), $decimalPlaces, var_export($coloured, true));
- Log::error($message);
-
- throw new FireflyException($message);
- }
-
- $decimalPlaces ??= 2;
- $coloured ??= true;
- $currency = new TransactionCurrency();
- $currency->symbol = $symbol;
- $currency->decimal_places = $decimalPlaces;
-
- return Amount::formatAnything($currency, $amount, $coloured);
- },
- ['is_safe' => ['html']]
- );
- }
-
- /**
- * Will format the amount by the currency related to the given account.
- */
- protected function formatAmountByCurrency(): TwigFunction
- {
- return new TwigFunction(
- 'formatAmountByCurrency',
- static function (TransactionCurrency $currency, string $amount, ?bool $coloured = null): string {
- $coloured ??= true;
-
- return Amount::formatAnything($currency, $amount, $coloured);
- },
- ['is_safe' => ['html']]
- );
- }
-
/**
* Use the code to format a currency.
*/
@@ -175,4 +118,61 @@ class AmountFormat extends AbstractExtension
['is_safe' => ['html']]
);
}
+
+ /**
+ * Will format the amount by the currency related to the given account.
+ */
+ protected function formatAmountByCurrency(): TwigFunction
+ {
+ return new TwigFunction(
+ 'formatAmountByCurrency',
+ static function (TransactionCurrency $currency, string $amount, ?bool $coloured = null): string {
+ $coloured ??= true;
+
+ return Amount::formatAnything($currency, $amount, $coloured);
+ },
+ ['is_safe' => ['html']]
+ );
+ }
+
+ /**
+ * Will format the amount by the currency related to the given account.
+ */
+ protected function formatAmountBySymbol(): TwigFunction
+ {
+ return new TwigFunction(
+ 'formatAmountBySymbol',
+ static function (string $amount, ?string $symbol = null, ?int $decimalPlaces = null, ?bool $coloured = null): string {
+
+ if (null === $symbol) {
+ $message = sprintf('formatAmountBySymbol("%s", %s, %d, %s) was called without a symbol. Please browse to /flush to clear your cache.', $amount, var_export($symbol, true), $decimalPlaces, var_export($coloured, true));
+ Log::error($message);
+ $currency = Amount::getPrimaryCurrency();
+ }
+ if (null !== $symbol) {
+ $decimalPlaces ??= 2;
+ $coloured ??= true;
+ $currency = new TransactionCurrency();
+ $currency->symbol = $symbol;
+ $currency->decimal_places = $decimalPlaces;
+ }
+
+ return Amount::formatAnything($currency, $amount, $coloured);
+ },
+ ['is_safe' => ['html']]
+ );
+ }
+
+ protected function formatAmountPlain(): TwigFilter
+ {
+ return new TwigFilter(
+ 'formatAmountPlain',
+ static function (string $string): string {
+ $currency = Amount::getPrimaryCurrency();
+
+ return Amount::formatAnything($currency, $string, false);
+ },
+ ['is_safe' => ['html']]
+ );
+ }
}
diff --git a/app/Support/Twig/General.php b/app/Support/Twig/General.php
index 6f71d6f578..485dfe1bd9 100644
--- a/app/Support/Twig/General.php
+++ b/app/Support/Twig/General.php
@@ -57,6 +57,91 @@ class General extends AbstractExtension
];
}
+ #[Override]
+ public function getFunctions(): array
+ {
+ return [
+ $this->phpdate(),
+ $this->activeRouteStrict(),
+ $this->activeRoutePartial(),
+ $this->activeRoutePartialObjectType(),
+ $this->menuOpenRoutePartial(),
+ $this->formatDate(),
+ $this->getMetaField(),
+ $this->hasRole(),
+ $this->getRootSearchOperator(),
+ $this->carbonize(),
+ ];
+ }
+
+ /**
+ * Will return "active" when a part of the route matches the argument.
+ * ie. "accounts" will match "accounts.index".
+ */
+ protected function activeRoutePartial(): TwigFunction
+ {
+ return new TwigFunction(
+ 'activeRoutePartial',
+ static function (): string {
+ $args = func_get_args();
+ $route = $args[0]; // name of the route.
+ $name = Route::getCurrentRoute()->getName() ?? '';
+ if (str_contains($name, $route)) {
+ return 'active';
+ }
+
+ return '';
+ }
+ );
+ }
+
+ /**
+ * This function will return "active" when the current route matches the first argument (even partly)
+ * but, the variable $objectType has been set and matches the second argument.
+ */
+ protected function activeRoutePartialObjectType(): TwigFunction
+ {
+ return new TwigFunction(
+ 'activeRoutePartialObjectType',
+ static function ($context): string {
+ [, $route, $objectType] = func_get_args();
+ $activeObjectType = $context['objectType'] ?? false;
+
+ if ($objectType === $activeObjectType
+ && false !== stripos(
+ (string)Route::getCurrentRoute()->getName(),
+ (string)$route
+ )) {
+ return 'active';
+ }
+
+ return '';
+ },
+ ['needs_context' => true]
+ );
+ }
+
+ /**
+ * Will return "active" when the current route matches the given argument
+ * exactly.
+ */
+ protected function activeRouteStrict(): TwigFunction
+ {
+ return new TwigFunction(
+ 'activeRouteStrict',
+ static function (): string {
+ $args = func_get_args();
+ $route = $args[0]; // name of the route.
+
+ if (\Route::getCurrentRoute()->getName() === $route) {
+ return 'active';
+ }
+
+ return '';
+ }
+ );
+ }
+
/**
* Show account balance. Only used on the front page of Firefly III.
*/
@@ -108,6 +193,29 @@ class General extends AbstractExtension
);
}
+ protected function carbonize(): TwigFunction
+ {
+ return new TwigFunction(
+ 'carbonize',
+ static fn (string $date): Carbon => new Carbon($date, config('app.timezone'))
+ );
+ }
+
+ /**
+ * Formats a string as a thing by converting it to a Carbon first.
+ */
+ protected function formatDate(): TwigFunction
+ {
+ return new TwigFunction(
+ 'formatDate',
+ static function (string $date, string $format): string {
+ $carbon = new Carbon($date);
+
+ return $carbon->isoFormat($format);
+ }
+ );
+ }
+
/**
* Used to convert 1024 to 1kb etc.
*/
@@ -131,6 +239,96 @@ class General extends AbstractExtension
);
}
+ /**
+ * TODO Remove me when v2 hits.
+ */
+ protected function getMetaField(): TwigFunction
+ {
+ return new TwigFunction(
+ 'accountGetMetaField',
+ static function (Account $account, string $field): string {
+ /** @var AccountRepositoryInterface $repository */
+ $repository = app(AccountRepositoryInterface::class);
+ $result = $repository->getMetaValue($account, $field);
+ if (null === $result) {
+ return '';
+ }
+
+ return $result;
+ }
+ );
+ }
+
+ protected function getRootSearchOperator(): TwigFunction
+ {
+ return new TwigFunction(
+ 'getRootSearchOperator',
+ static function (string $operator): string {
+ $result = OperatorQuerySearch::getRootOperator($operator);
+
+ return str_replace('-', 'not_', $result);
+ }
+ );
+ }
+
+ /**
+ * Will return true if the user is of role X.
+ */
+ protected function hasRole(): TwigFunction
+ {
+ return new TwigFunction(
+ 'hasRole',
+ static function (string $role): bool {
+ $repository = app(UserRepositoryInterface::class);
+ if ($repository->hasRole(auth()->user(), $role)) {
+ return true;
+ }
+
+ return false;
+ }
+ );
+ }
+
+ protected function markdown(): TwigFilter
+ {
+ return new TwigFilter(
+ 'markdown',
+ static function (string $text): string {
+ $converter = new GithubFlavoredMarkdownConverter(
+ [
+ 'allow_unsafe_links' => false,
+ 'max_nesting_level' => 5,
+ 'html_input' => 'escape',
+ ]
+ );
+
+ return (string)$converter->convert($text);
+ },
+ ['is_safe' => ['html']]
+ );
+ }
+
+ /**
+ * Will return "menu-open" when a part of the route matches the argument.
+ * ie. "accounts" will match "accounts.index".
+ */
+ protected function menuOpenRoutePartial(): TwigFunction
+ {
+ return new TwigFunction(
+ 'menuOpenRoutePartial',
+ static function (): string {
+ $args = func_get_args();
+ $route = $args[0]; // name of the route.
+ $name = Route::getCurrentRoute()->getName() ?? '';
+ if (str_contains($name, $route)) {
+ return 'menu-open';
+ }
+
+ return '';
+ }
+ );
+ }
+
/**
* Show icon with attachment.
*
@@ -154,25 +352,6 @@ class General extends AbstractExtension
);
}
- protected function markdown(): TwigFilter
- {
- return new TwigFilter(
- 'markdown',
- static function (string $text): string {
- $converter = new GithubFlavoredMarkdownConverter(
- [
- 'allow_unsafe_links' => false,
- 'max_nesting_level' => 5,
- 'html_input' => 'escape',
- ]
- );
-
- return (string)$converter->convert($text);
- },
- ['is_safe' => ['html']]
- );
- }
-
/**
* Show URL host name
*/
@@ -195,23 +374,6 @@ class General extends AbstractExtension
);
}
- #[Override]
- public function getFunctions(): array
- {
- return [
- $this->phpdate(),
- $this->activeRouteStrict(),
- $this->activeRoutePartial(),
- $this->activeRoutePartialObjectType(),
- $this->menuOpenRoutePartial(),
- $this->formatDate(),
- $this->getMetaField(),
- $this->hasRole(),
- $this->getRootSearchOperator(),
- $this->carbonize(),
- ];
- }
-
/**
* Basic example thing for some views.
*/
@@ -222,166 +384,4 @@ class General extends AbstractExtension
static fn (string $str): string => date($str)
);
}
-
- /**
- * Will return "active" when the current route matches the given argument
- * exactly.
- */
- protected function activeRouteStrict(): TwigFunction
- {
- return new TwigFunction(
- 'activeRouteStrict',
- static function (): string {
- $args = func_get_args();
- $route = $args[0]; // name of the route.
-
- if (\Route::getCurrentRoute()->getName() === $route) {
- return 'active';
- }
-
- return '';
- }
- );
- }
-
- /**
- * Will return "active" when a part of the route matches the argument.
- * ie. "accounts" will match "accounts.index".
- */
- protected function activeRoutePartial(): TwigFunction
- {
- return new TwigFunction(
- 'activeRoutePartial',
- static function (): string {
- $args = func_get_args();
- $route = $args[0]; // name of the route.
- $name = Route::getCurrentRoute()->getName() ?? '';
- if (str_contains($name, $route)) {
- return 'active';
- }
-
- return '';
- }
- );
- }
-
- /**
- * This function will return "active" when the current route matches the first argument (even partly)
- * but, the variable $objectType has been set and matches the second argument.
- */
- protected function activeRoutePartialObjectType(): TwigFunction
- {
- return new TwigFunction(
- 'activeRoutePartialObjectType',
- static function ($context): string {
- [, $route, $objectType] = func_get_args();
- $activeObjectType = $context['objectType'] ?? false;
-
- if ($objectType === $activeObjectType
- && false !== stripos(
- (string)Route::getCurrentRoute()->getName(),
- (string)$route
- )) {
- return 'active';
- }
-
- return '';
- },
- ['needs_context' => true]
- );
- }
-
- /**
- * Will return "menu-open" when a part of the route matches the argument.
- * ie. "accounts" will match "accounts.index".
- */
- protected function menuOpenRoutePartial(): TwigFunction
- {
- return new TwigFunction(
- 'menuOpenRoutePartial',
- static function (): string {
- $args = func_get_args();
- $route = $args[0]; // name of the route.
- $name = Route::getCurrentRoute()->getName() ?? '';
- if (str_contains($name, $route)) {
- return 'menu-open';
- }
-
- return '';
- }
- );
- }
-
- /**
- * Formats a string as a thing by converting it to a Carbon first.
- */
- protected function formatDate(): TwigFunction
- {
- return new TwigFunction(
- 'formatDate',
- static function (string $date, string $format): string {
- $carbon = new Carbon($date);
-
- return $carbon->isoFormat($format);
- }
- );
- }
-
- /**
- * TODO Remove me when v2 hits.
- */
- protected function getMetaField(): TwigFunction
- {
- return new TwigFunction(
- 'accountGetMetaField',
- static function (Account $account, string $field): string {
- /** @var AccountRepositoryInterface $repository */
- $repository = app(AccountRepositoryInterface::class);
- $result = $repository->getMetaValue($account, $field);
- if (null === $result) {
- return '';
- }
-
- return $result;
- }
- );
- }
-
- /**
- * Will return true if the user is of role X.
- */
- protected function hasRole(): TwigFunction
- {
- return new TwigFunction(
- 'hasRole',
- static function (string $role): bool {
- $repository = app(UserRepositoryInterface::class);
- if ($repository->hasRole(auth()->user(), $role)) {
- return true;
- }
-
- return false;
- }
- );
- }
-
- protected function getRootSearchOperator(): TwigFunction
- {
- return new TwigFunction(
- 'getRootSearchOperator',
- static function (string $operator): string {
- $result = OperatorQuerySearch::getRootOperator($operator);
-
- return str_replace('-', 'not_', $result);
- }
- );
- }
-
- protected function carbonize(): TwigFunction
- {
- return new TwigFunction(
- 'carbonize',
- static fn (string $date): Carbon => new Carbon($date, config('app.timezone'))
- );
- }
}
diff --git a/app/Support/Twig/Rule.php b/app/Support/Twig/Rule.php
index c833745d40..ea60a67a3e 100644
--- a/app/Support/Twig/Rule.php
+++ b/app/Support/Twig/Rule.php
@@ -23,24 +23,33 @@ declare(strict_types=1);
namespace FireflyIII\Support\Twig;
-use Twig\Extension\AbstractExtension;
-use Twig\TwigFunction;
use Config;
use Override;
+use Twig\Extension\AbstractExtension;
+use Twig\TwigFunction;
/**
* Class Rule.
*/
class Rule extends AbstractExtension
{
- #[Override]
- public function getFunctions(): array
+ public function allActionTriggers(): TwigFunction
{
- return [
- $this->allJournalTriggers(),
- $this->allRuleTriggers(),
- $this->allActionTriggers(),
- ];
+ return new TwigFunction(
+ 'allRuleActions',
+ static function () {
+ // array of valid values for actions
+ $ruleActions = array_keys(Config::get('firefly.rule-actions'));
+ $possibleActions = [];
+ foreach ($ruleActions as $key) {
+ $possibleActions[$key] = (string)trans('firefly.rule_action_'.$key.'_choice');
+ }
+ unset($ruleActions);
+ asort($possibleActions);
+
+ return $possibleActions;
+ }
+ );
}
public function allJournalTriggers(): TwigFunction
@@ -48,9 +57,9 @@ class Rule extends AbstractExtension
return new TwigFunction(
'allJournalTriggers',
static fn () => [
- 'store-journal' => (string) trans('firefly.rule_trigger_store_journal'),
- 'update-journal' => (string) trans('firefly.rule_trigger_update_journal'),
- 'manual-activation' => (string) trans('firefly.rule_trigger_manual'),
+ 'store-journal' => (string)trans('firefly.rule_trigger_store_journal'),
+ 'update-journal' => (string)trans('firefly.rule_trigger_update_journal'),
+ 'manual-activation' => (string)trans('firefly.rule_trigger_manual'),
]
);
}
@@ -64,7 +73,7 @@ class Rule extends AbstractExtension
$possibleTriggers = [];
foreach ($ruleTriggers as $key) {
if ('user_action' !== $key) {
- $possibleTriggers[$key] = (string) trans('firefly.rule_trigger_'.$key.'_choice');
+ $possibleTriggers[$key] = (string)trans('firefly.rule_trigger_'.$key.'_choice');
}
}
unset($ruleTriggers);
@@ -75,22 +84,13 @@ class Rule extends AbstractExtension
);
}
- public function allActionTriggers(): TwigFunction
+ #[Override]
+ public function getFunctions(): array
{
- return new TwigFunction(
- 'allRuleActions',
- static function () {
- // array of valid values for actions
- $ruleActions = array_keys(Config::get('firefly.rule-actions'));
- $possibleActions = [];
- foreach ($ruleActions as $key) {
- $possibleActions[$key] = (string) trans('firefly.rule_action_'.$key.'_choice');
- }
- unset($ruleActions);
- asort($possibleActions);
-
- return $possibleActions;
- }
- );
+ return [
+ $this->allJournalTriggers(),
+ $this->allRuleTriggers(),
+ $this->allActionTriggers(),
+ ];
}
}
diff --git a/app/Support/Twig/TransactionGroupTwig.php b/app/Support/Twig/TransactionGroupTwig.php
index 81cf8db231..3033a84872 100644
--- a/app/Support/Twig/TransactionGroupTwig.php
+++ b/app/Support/Twig/TransactionGroupTwig.php
@@ -31,9 +31,9 @@ use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionJournalMeta;
use Illuminate\Support\Facades\DB;
+use Override;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
-use Override;
use function Safe\json_decode;
@@ -76,70 +76,63 @@ class TransactionGroupTwig extends AbstractExtension
);
}
- /**
- * Generate normal amount for transaction from a transaction group.
- */
- private function normalJournalArrayAmount(array $array): string
+ public function journalGetMetaDate(): TwigFunction
{
- $type = $array['transaction_type_type'] ?? TransactionTypeEnum::WITHDRAWAL->value;
- $amount = $array['amount'] ?? '0';
- $colored = true;
- $sourceType = $array['source_account_type'] ?? 'invalid';
- $amount = $this->signAmount($amount, $type, $sourceType);
+ return new TwigFunction(
+ 'journalGetMetaDate',
+ static function (int $journalId, string $metaField) {
+ /** @var null|TransactionJournalMeta $entry */
+ $entry = DB::table('journal_meta')
+ ->where('name', $metaField)
+ ->where('transaction_journal_id', $journalId)
+ ->whereNull('deleted_at')
+ ->first()
+ ;
+ if (null === $entry) {
+ return today(config('app.timezone'));
+ }
- if (TransactionTypeEnum::TRANSFER->value === $type) {
- $colored = false;
- }
-
- $result = app('amount')->formatFlat($array['currency_symbol'], (int) $array['currency_decimal_places'], $amount, $colored);
- if (TransactionTypeEnum::TRANSFER->value === $type) {
- return sprintf('%s', $result);
- }
-
- return $result;
+ return new Carbon(json_decode((string)$entry->data, false));
+ }
+ );
}
- private function signAmount(string $amount, string $transactionType, string $sourceType): string
+ public function journalGetMetaField(): TwigFunction
{
- // withdrawals stay negative
- if (TransactionTypeEnum::WITHDRAWAL->value !== $transactionType) {
- $amount = bcmul($amount, '-1');
- }
+ return new TwigFunction(
+ 'journalGetMetaField',
+ static function (int $journalId, string $metaField) {
+ /** @var null|TransactionJournalMeta $entry */
+ $entry = DB::table('journal_meta')
+ ->where('name', $metaField)
+ ->where('transaction_journal_id', $journalId)
+ ->whereNull('deleted_at')
+ ->first()
+ ;
+ if (null === $entry) {
+ return '';
+ }
- // opening balance and it comes from initial balance? its expense.
- if (TransactionTypeEnum::OPENING_BALANCE->value === $transactionType && AccountTypeEnum::INITIAL_BALANCE->value !== $sourceType) {
- $amount = bcmul($amount, '-1');
- }
-
- // reconciliation and it comes from reconciliation?
- if (TransactionTypeEnum::RECONCILIATION->value === $transactionType && AccountTypeEnum::RECONCILIATION->value !== $sourceType) {
- return bcmul($amount, '-1');
- }
-
- return $amount;
+ return json_decode((string)$entry->data, true);
+ }
+ );
}
- /**
- * Generate foreign amount for transaction from a transaction group.
- */
- private function foreignJournalArrayAmount(array $array): string
+ public function journalHasMeta(): TwigFunction
{
- $type = $array['transaction_type_type'] ?? TransactionTypeEnum::WITHDRAWAL->value;
- $amount = $array['foreign_amount'] ?? '0';
- $colored = true;
+ return new TwigFunction(
+ 'journalHasMeta',
+ static function (int $journalId, string $metaField) {
+ $count = DB::table('journal_meta')
+ ->where('name', $metaField)
+ ->where('transaction_journal_id', $journalId)
+ ->whereNull('deleted_at')
+ ->count()
+ ;
- $sourceType = $array['source_account_type'] ?? 'invalid';
- $amount = $this->signAmount($amount, $type, $sourceType);
-
- if (TransactionTypeEnum::TRANSFER->value === $type) {
- $colored = false;
- }
- $result = app('amount')->formatFlat($array['foreign_currency_symbol'], (int) $array['foreign_currency_decimal_places'], $amount, $colored);
- if (TransactionTypeEnum::TRANSFER->value === $type) {
- return sprintf('%s', $result);
- }
-
- return $result;
+ return 1 === $count;
+ }
+ );
}
/**
@@ -164,25 +157,21 @@ class TransactionGroupTwig extends AbstractExtension
}
/**
- * Generate normal amount for transaction from a transaction group.
+ * Generate foreign amount for transaction from a transaction group.
*/
- private function normalJournalObjectAmount(TransactionJournal $journal): string
+ private function foreignJournalArrayAmount(array $array): string
{
- $type = $journal->transactionType->type;
-
- /** @var Transaction $first */
- $first = $journal->transactions()->where('amount', '<', 0)->first();
- $currency = $journal->transactionCurrency;
- $amount = $first->amount ?? '0';
+ $type = $array['transaction_type_type'] ?? TransactionTypeEnum::WITHDRAWAL->value;
+ $amount = $array['foreign_amount'] ?? '0';
$colored = true;
- $sourceType = $first->account->accountType()->first()->type;
+ $sourceType = $array['source_account_type'] ?? 'invalid';
$amount = $this->signAmount($amount, $type, $sourceType);
if (TransactionTypeEnum::TRANSFER->value === $type) {
$colored = false;
}
- $result = app('amount')->formatFlat($currency->symbol, $currency->decimal_places, $amount, $colored);
+ $result = app('amount')->formatFlat($array['foreign_currency_symbol'], (int)$array['foreign_currency_decimal_places'], $amount, $colored);
if (TransactionTypeEnum::TRANSFER->value === $type) {
return sprintf('%s', $result);
}
@@ -190,14 +179,6 @@ class TransactionGroupTwig extends AbstractExtension
return $result;
}
- private function journalObjectHasForeign(TransactionJournal $journal): bool
- {
- /** @var Transaction $first */
- $first = $journal->transactions()->where('amount', '<', 0)->first();
-
- return '' !== $first->foreign_amount;
- }
-
/**
* Generate foreign amount for journal from a transaction group.
*/
@@ -225,62 +206,81 @@ class TransactionGroupTwig extends AbstractExtension
return $result;
}
- public function journalHasMeta(): TwigFunction
+ private function journalObjectHasForeign(TransactionJournal $journal): bool
{
- return new TwigFunction(
- 'journalHasMeta',
- static function (int $journalId, string $metaField) {
- $count = DB::table('journal_meta')
- ->where('name', $metaField)
- ->where('transaction_journal_id', $journalId)
- ->whereNull('deleted_at')
- ->count()
- ;
+ /** @var Transaction $first */
+ $first = $journal->transactions()->where('amount', '<', 0)->first();
- return 1 === $count;
- }
- );
+ return '' !== $first->foreign_amount;
}
- public function journalGetMetaDate(): TwigFunction
+ /**
+ * Generate normal amount for transaction from a transaction group.
+ */
+ private function normalJournalArrayAmount(array $array): string
{
- return new TwigFunction(
- 'journalGetMetaDate',
- static function (int $journalId, string $metaField) {
- /** @var null|TransactionJournalMeta $entry */
- $entry = DB::table('journal_meta')
- ->where('name', $metaField)
- ->where('transaction_journal_id', $journalId)
- ->whereNull('deleted_at')
- ->first()
- ;
- if (null === $entry) {
- return today(config('app.timezone'));
- }
+ $type = $array['transaction_type_type'] ?? TransactionTypeEnum::WITHDRAWAL->value;
+ $amount = $array['amount'] ?? '0';
+ $colored = true;
+ $sourceType = $array['source_account_type'] ?? 'invalid';
+ $amount = $this->signAmount($amount, $type, $sourceType);
- return new Carbon(json_decode((string) $entry->data, false));
- }
- );
+ if (TransactionTypeEnum::TRANSFER->value === $type) {
+ $colored = false;
+ }
+
+ $result = app('amount')->formatFlat($array['currency_symbol'], (int)$array['currency_decimal_places'], $amount, $colored);
+ if (TransactionTypeEnum::TRANSFER->value === $type) {
+ return sprintf('%s', $result);
+ }
+
+ return $result;
}
- public function journalGetMetaField(): TwigFunction
+ /**
+ * Generate normal amount for transaction from a transaction group.
+ */
+ private function normalJournalObjectAmount(TransactionJournal $journal): string
{
- return new TwigFunction(
- 'journalGetMetaField',
- static function (int $journalId, string $metaField) {
- /** @var null|TransactionJournalMeta $entry */
- $entry = DB::table('journal_meta')
- ->where('name', $metaField)
- ->where('transaction_journal_id', $journalId)
- ->whereNull('deleted_at')
- ->first()
- ;
- if (null === $entry) {
- return '';
- }
+ $type = $journal->transactionType->type;
- return json_decode((string) $entry->data, true);
- }
- );
+ /** @var Transaction $first */
+ $first = $journal->transactions()->where('amount', '<', 0)->first();
+ $currency = $journal->transactionCurrency;
+ $amount = $first->amount ?? '0';
+ $colored = true;
+ $sourceType = $first->account->accountType()->first()->type;
+
+ $amount = $this->signAmount($amount, $type, $sourceType);
+
+ if (TransactionTypeEnum::TRANSFER->value === $type) {
+ $colored = false;
+ }
+ $result = app('amount')->formatFlat($currency->symbol, $currency->decimal_places, $amount, $colored);
+ if (TransactionTypeEnum::TRANSFER->value === $type) {
+ return sprintf('%s', $result);
+ }
+
+ return $result;
+ }
+
+ private function signAmount(string $amount, string $transactionType, string $sourceType): string
+ {
+ // withdrawals stay negative
+ if (TransactionTypeEnum::WITHDRAWAL->value !== $transactionType) {
+ $amount = bcmul($amount, '-1');
+ }
+
+ // opening balance and it comes from initial balance? its expense.
+ if (TransactionTypeEnum::OPENING_BALANCE->value === $transactionType && AccountTypeEnum::INITIAL_BALANCE->value !== $sourceType) {
+ $amount = bcmul($amount, '-1');
+ }
+
+ // reconciliation and it comes from reconciliation?
+ if (TransactionTypeEnum::RECONCILIATION->value === $transactionType && AccountTypeEnum::RECONCILIATION->value !== $sourceType) {
+ return bcmul($amount, '-1');
+ }
+
+ return $amount;
}
}
diff --git a/app/Support/Twig/Translation.php b/app/Support/Twig/Translation.php
index bb19890ff9..d316895ed7 100644
--- a/app/Support/Twig/Translation.php
+++ b/app/Support/Twig/Translation.php
@@ -23,10 +23,10 @@ declare(strict_types=1);
namespace FireflyIII\Support\Twig;
+use Override;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
-use Override;
/**
* Class Budget.
@@ -39,7 +39,7 @@ class Translation extends AbstractExtension
return [
new TwigFilter(
'_',
- static fn ($name) => (string) trans(sprintf('firefly.%s', $name)),
+ static fn ($name) => (string)trans(sprintf('firefly.%s', $name)),
['is_safe' => ['html']]
),
];
diff --git a/app/TransactionRules/Engine/SearchRuleEngine.php b/app/TransactionRules/Engine/SearchRuleEngine.php
index c4b4822ba4..7843d1ffca 100644
--- a/app/TransactionRules/Engine/SearchRuleEngine.php
+++ b/app/TransactionRules/Engine/SearchRuleEngine.php
@@ -508,9 +508,11 @@ class SearchRuleEngine implements RuleEngineInterface
{
Log::debug(sprintf('Going to fire group #%d with %d rule(s)', $group->id, $group->rules->count()));
+ $rules = $group->rules()->orderBy('order', 'ASC')->get();
+
/** @var Rule $rule */
- foreach ($group->rules as $rule) {
- Log::debug(sprintf('Going to fire rule #%d from group #%d', $rule->id, $group->id));
+ foreach ($rules as $rule) {
+ Log::debug(sprintf('Going to fire rule #%d with order #%d from group #%d', $rule->id, $rule->order, $group->id));
$result = $this->fireRule($rule);
if (true === $result && true === $rule->stop_processing) {
Log::debug(sprintf('The rule was triggered and rule->stop_processing = true, so group #%d will stop processing further rules.', $group->id));
diff --git a/app/Transformers/PiggyBankEventTransformer.php b/app/Transformers/PiggyBankEventTransformer.php
index a9724ddc57..a0a43ecc59 100644
--- a/app/Transformers/PiggyBankEventTransformer.php
+++ b/app/Transformers/PiggyBankEventTransformer.php
@@ -35,7 +35,7 @@ use FireflyIII\Support\Facades\Steam;
*/
class PiggyBankEventTransformer extends AbstractTransformer
{
- private TransactionCurrency $primaryCurrency;
+ private readonly TransactionCurrency $primaryCurrency;
private bool $convertToPrimary = false;
/**
diff --git a/app/Validation/Account/DepositValidation.php b/app/Validation/Account/DepositValidation.php
index 87e2e69f43..36cd7e7d35 100644
--- a/app/Validation/Account/DepositValidation.php
+++ b/app/Validation/Account/DepositValidation.php
@@ -27,6 +27,7 @@ namespace FireflyIII\Validation\Account;
use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
+use Illuminate\Support\Facades\Log;
/**
* Trait DepositValidation
@@ -40,7 +41,7 @@ trait DepositValidation
$accountName = array_key_exists('name', $array) ? $array['name'] : null;
$accountIban = array_key_exists('iban', $array) ? $array['iban'] : null;
- app('log')->debug('Now in validateDepositDestination', $array);
+ Log::debug('Now in validateDepositDestination', $array);
// source can be any of the following types.
$validTypes = $this->combinations[$this->transactionType][$this->source->accountType->type] ?? [];
@@ -48,12 +49,12 @@ trait DepositValidation
// if both values are NULL we return false,
// because the destination of a deposit can't be created.
$this->destError = (string) trans('validation.deposit_dest_need_data');
- app('log')->error('Both values are NULL, cant create deposit destination.');
+ Log::error('Both values are NULL, cant create deposit destination.');
$result = false;
}
// if the account can be created anyway we don't need to search.
if (null === $result && true === $this->canCreateTypes($validTypes)) {
- app('log')->debug('Can create some of these types, so return true.');
+ Log::debug('Can create some of these types, so return true.');
$result = true;
}
@@ -61,17 +62,17 @@ trait DepositValidation
// otherwise try to find the account:
$search = $this->findExistingAccount($validTypes, $array);
if (null === $search) {
- app('log')->debug('findExistingAccount() returned NULL, so the result is false.');
+ Log::debug('findExistingAccount() returned NULL, so the result is false.');
$this->destError = (string) trans('validation.deposit_dest_bad_data', ['id' => $accountId, 'name' => $accountName]);
$result = false;
}
if (null !== $search) {
- app('log')->debug(sprintf('findExistingAccount() returned #%d ("%s"), so the result is true.', $search->id, $search->name));
+ Log::debug(sprintf('findExistingAccount() returned #%d ("%s"), so the result is true.', $search->id, $search->name));
$this->setDestination($search);
$result = true;
}
}
- app('log')->debug(sprintf('validateDepositDestination will return %s', var_export($result, true)));
+ Log::debug(sprintf('validateDepositDestination will return %s', var_export($result, true)));
return $result;
}
@@ -92,7 +93,7 @@ trait DepositValidation
$accountName = array_key_exists('name', $array) ? $array['name'] : null;
$accountIban = array_key_exists('iban', $array) ? $array['iban'] : null;
$accountNumber = array_key_exists('number', $array) ? $array['number'] : null;
- app('log')->debug('Now in validateDepositSource', $array);
+ Log::debug('Now in validateDepositSource', $array);
// null = we found nothing at all or didn't even search
// false = invalid results
@@ -114,7 +115,7 @@ trait DepositValidation
// if there is an iban, it can only be in use by a valid source type, or we will fail.
if (null !== $accountIban && '' !== $accountIban) {
- app('log')->debug('Check if there is not already another account with this IBAN');
+ Log::debug('Check if there is not already another account with this IBAN');
$existing = $this->findExistingAccount($validTypes, ['iban' => $accountIban], true);
if (null !== $existing) {
$this->sourceError = (string) trans('validation.deposit_src_iban_exists');
@@ -128,11 +129,14 @@ trait DepositValidation
if (null !== $accountId) {
$search = $this->accountRepository->find($accountId);
if (null !== $search && !in_array($search->accountType->type, $validTypes, true)) {
- app('log')->debug(sprintf('User submitted an ID (#%d), which is a "%s", so this is not a valid source.', $accountId, $search->accountType->type));
- app('log')->debug(sprintf('Firefly III accepts ID #%d as valid account data.', $accountId));
+ Log::debug(sprintf('User submitted an ID (#%d), which is a "%s", so this is not a valid source.', $accountId, $search->accountType->type));
+ Log::debug(sprintf('Firefly III does not accept ID #%d as valid account data.', $accountId));
+ // #10921 Set result false
+ $this->sourceError = (string) trans('validation.withdrawal_source_bad_data', ['id' => $accountId, 'name' => $accountName]);
+ $result = false;
}
if (null !== $search && in_array($search->accountType->type, $validTypes, true)) {
- app('log')->debug('ID result is not null and seems valid, save as source account.');
+ Log::debug('ID result is not null and seems valid, save as source account.');
$this->setSource($search);
$result = true;
}
@@ -142,11 +146,11 @@ trait DepositValidation
if (null !== $accountIban) {
$search = $this->accountRepository->findByIbanNull($accountIban, $validTypes);
if (null !== $search && !in_array($search->accountType->type, $validTypes, true)) {
- app('log')->debug(sprintf('User submitted IBAN ("%s"), which is a "%s", so this is not a valid source.', $accountIban, $search->accountType->type));
+ Log::debug(sprintf('User submitted IBAN ("%s"), which is a "%s", so this is not a valid source.', $accountIban, $search->accountType->type));
$result = false;
}
if (null !== $search && in_array($search->accountType->type, $validTypes, true)) {
- app('log')->debug('IBAN result is not null and seems valid, save as source account.');
+ Log::debug('IBAN result is not null and seems valid, save as source account.');
$this->setSource($search);
$result = true;
}
@@ -156,13 +160,13 @@ trait DepositValidation
if (null !== $accountNumber && '' !== $accountNumber) {
$search = $this->accountRepository->findByAccountNumber($accountNumber, $validTypes);
if (null !== $search && !in_array($search->accountType->type, $validTypes, true)) {
- app('log')->debug(
+ Log::debug(
sprintf('User submitted number ("%s"), which is a "%s", so this is not a valid source.', $accountNumber, $search->accountType->type)
);
$result = false;
}
if (null !== $search && in_array($search->accountType->type, $validTypes, true)) {
- app('log')->debug('Number result is not null and seems valid, save as source account.');
+ Log::debug('Number result is not null and seems valid, save as source account.');
$this->setSource($search);
$result = true;
}
diff --git a/app/Validation/Account/OBValidation.php b/app/Validation/Account/OBValidation.php
index a3f32afeb0..b0633c63f9 100644
--- a/app/Validation/Account/OBValidation.php
+++ b/app/Validation/Account/OBValidation.php
@@ -27,6 +27,7 @@ namespace FireflyIII\Validation\Account;
use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
+use Illuminate\Support\Facades\Log;
/**
* Trait OBValidation
@@ -38,7 +39,7 @@ trait OBValidation
$result = null;
$accountId = array_key_exists('id', $array) ? $array['id'] : null;
$accountName = array_key_exists('name', $array) ? $array['name'] : null;
- app('log')->debug('Now in validateOBDestination', $array);
+ Log::debug('Now in validateOBDestination', $array);
// source can be any of the following types.
$validTypes = $this->combinations[$this->transactionType][$this->source?->accountType->type] ?? [];
@@ -46,12 +47,12 @@ trait OBValidation
// if both values are NULL we return false,
// because the destination of a deposit can't be created.
$this->destError = (string) trans('validation.ob_dest_need_data');
- app('log')->error('Both values are NULL, cant create OB destination.');
+ Log::error('Both values are NULL, cant create OB destination.');
$result = false;
}
// if the account can be created anyway we don't need to search.
if (null === $result && true === $this->canCreateTypes($validTypes)) {
- app('log')->debug('Can create some of these types, so return true.');
+ Log::debug('Can create some of these types, so return true.');
$result = true;
}
@@ -59,17 +60,17 @@ trait OBValidation
// otherwise try to find the account:
$search = $this->findExistingAccount($validTypes, $array);
if (null === $search) {
- app('log')->debug('findExistingAccount() returned NULL, so the result is false.', $validTypes);
+ Log::debug('findExistingAccount() returned NULL, so the result is false.', $validTypes);
$this->destError = (string) trans('validation.ob_dest_bad_data', ['id' => $accountId, 'name' => $accountName]);
$result = false;
}
if (null !== $search) {
- app('log')->debug(sprintf('findExistingAccount() returned #%d ("%s"), so the result is true.', $search->id, $search->name));
+ Log::debug(sprintf('findExistingAccount() returned #%d ("%s"), so the result is true.', $search->id, $search->name));
$this->setDestination($search);
$result = true;
}
}
- app('log')->debug(sprintf('validateOBDestination(%d, "%s") will return %s', $accountId, $accountName, var_export($result, true)));
+ Log::debug(sprintf('validateOBDestination(%d, "%s") will return %s', $accountId, $accountName, var_export($result, true)));
return $result;
}
@@ -84,7 +85,7 @@ trait OBValidation
{
$accountId = array_key_exists('id', $array) ? $array['id'] : null;
$accountName = array_key_exists('name', $array) ? $array['name'] : null;
- app('log')->debug('Now in validateOBSource', $array);
+ Log::debug('Now in validateOBSource', $array);
$result = null;
// source can be any of the following types.
$validTypes = array_keys($this->combinations[$this->transactionType]);
@@ -100,19 +101,19 @@ trait OBValidation
// if the user submits an ID only but that ID is not of the correct type,
// return false.
if (null !== $accountId && null === $accountName) {
- app('log')->debug('Source ID is not null, but name is null.');
+ Log::debug('Source ID is not null, but name is null.');
$search = $this->accountRepository->find($accountId);
// the source resulted in an account, but it's not of a valid type.
if (null !== $search && !in_array($search->accountType->type, $validTypes, true)) {
$message = sprintf('User submitted only an ID (#%d), which is a "%s", so this is not a valid source.', $accountId, $search->accountType->type);
- app('log')->debug($message);
+ Log::debug($message);
$this->sourceError = $message;
$result = false;
}
// the source resulted in an account, AND it's of a valid type.
if (null !== $search && in_array($search->accountType->type, $validTypes, true)) {
- app('log')->debug(sprintf('Found account of correct type: #%d, "%s"', $search->id, $search->name));
+ Log::debug(sprintf('Found account of correct type: #%d, "%s"', $search->id, $search->name));
$this->setSource($search);
$result = true;
}
@@ -120,7 +121,7 @@ trait OBValidation
// if the account can be created anyway we don't need to search.
if (null === $result && true === $this->canCreateTypes($validTypes)) {
- app('log')->debug('Result is still null.');
+ Log::debug('Result is still null.');
$result = true;
// set the source to be a (dummy) initial balance account.
diff --git a/app/Validation/Account/WithdrawalValidation.php b/app/Validation/Account/WithdrawalValidation.php
index 9456e4ecf9..ac2a06d825 100644
--- a/app/Validation/Account/WithdrawalValidation.php
+++ b/app/Validation/Account/WithdrawalValidation.php
@@ -26,6 +26,7 @@ namespace FireflyIII\Validation\Account;
use FireflyIII\Enums\AccountTypeEnum;
use FireflyIII\Models\Account;
+use Illuminate\Support\Facades\Log;
/**
* Trait WithdrawalValidation
@@ -37,14 +38,14 @@ trait WithdrawalValidation
$accountId = array_key_exists('id', $array) ? $array['id'] : null;
$accountName = array_key_exists('name', $array) ? $array['name'] : null;
$accountIban = array_key_exists('iban', $array) ? $array['iban'] : null;
- app('log')->debug('Now in validateGenericSource', $array);
+ Log::debug('Now in validateGenericSource', $array);
// source can be any of the following types.
$validTypes = [AccountTypeEnum::ASSET->value, AccountTypeEnum::REVENUE->value, AccountTypeEnum::LOAN->value, AccountTypeEnum::DEBT->value, AccountTypeEnum::MORTGAGE->value];
if (null === $accountId && null === $accountName && null === $accountIban && false === $this->canCreateTypes($validTypes)) {
// if both values are NULL we return TRUE
// because we assume the user doesn't want to submit / change anything.
$this->sourceError = (string) trans('validation.withdrawal_source_need_data');
- app('log')->warning('[a] Not a valid source. Need more data.');
+ Log::warning('[a] Not a valid source. Need more data.');
return false;
}
@@ -53,12 +54,12 @@ trait WithdrawalValidation
$search = $this->findExistingAccount($validTypes, $array);
if (null === $search) {
$this->sourceError = (string) trans('validation.withdrawal_source_bad_data', ['id' => $accountId, 'name' => $accountName]);
- app('log')->warning('Not a valid source. Cant find it.', $validTypes);
+ Log::warning('Not a valid source. Cant find it.', $validTypes);
return false;
}
$this->setSource($search);
- app('log')->debug('Valid source account!');
+ Log::debug('Valid source account!');
return true;
}
@@ -73,10 +74,10 @@ trait WithdrawalValidation
$accountName = array_key_exists('name', $array) ? $array['name'] : null;
$accountIban = array_key_exists('iban', $array) ? $array['iban'] : null;
$accountNumber = array_key_exists('number', $array) ? $array['number'] : null;
- app('log')->debug('Now in validateWithdrawalDestination()', $array);
+ Log::debug('Now in validateWithdrawalDestination()', $array);
// source can be any of the following types.
$validTypes = $this->combinations[$this->transactionType][$this->source->accountType->type] ?? [];
- app('log')->debug('Source type can be: ', $validTypes);
+ Log::debug('Source type can be: ', $validTypes);
if (null === $accountId && null === $accountName && null === $accountIban && null === $accountNumber && false === $this->canCreateTypes($validTypes)) {
// if both values are NULL return false,
// because the destination of a withdrawal can never be created automatically.
@@ -86,7 +87,7 @@ trait WithdrawalValidation
}
// if there's an ID it must be of the "validTypes".
- if (null !== $accountId && 0 !== $accountId) {
+ if (null !== $accountId && 0 !== $accountId && $accountId !== $this->source->id) {
$found = $this->accountRepository->find($accountId);
if (null !== $found) {
$type = $found->accountType->type;
@@ -104,7 +105,7 @@ trait WithdrawalValidation
// if there is an iban, it can only be in use by a valid destination type, or we will fail.
// the inverse of $validTypes is
if (null !== $accountIban && '' !== $accountIban) {
- app('log')->debug('Check if there is not already an account with this IBAN');
+ Log::debug('Check if there is not already an account with this IBAN');
// the inverse flag reverses the search, searching for everything that is NOT a valid type.
$existing = $this->findExistingAccount($validTypes, ['iban' => $accountIban], true);
if (null !== $existing) {
@@ -125,14 +126,14 @@ trait WithdrawalValidation
$accountIban = array_key_exists('iban', $array) ? $array['iban'] : null;
$accountNumber = array_key_exists('number', $array) ? $array['number'] : null;
- app('log')->debug('Now in validateWithdrawalSource', $array);
+ Log::debug('Now in validateWithdrawalSource', $array);
// source can be any of the following types.
$validTypes = array_keys($this->combinations[$this->transactionType]);
if (null === $accountId && null === $accountName && null === $accountNumber && null === $accountIban && false === $this->canCreateTypes($validTypes)) {
// if both values are NULL we return false,
// because the source of a withdrawal can't be created.
$this->sourceError = (string) trans('validation.withdrawal_source_need_data');
- app('log')->warning('[b] Not a valid source. Need more data.');
+ Log::warning('[b] Not a valid source. Need more data.');
return false;
}
@@ -141,12 +142,12 @@ trait WithdrawalValidation
$search = $this->findExistingAccount($validTypes, $array);
if (null === $search) {
$this->sourceError = (string) trans('validation.withdrawal_source_bad_data', ['id' => $accountId, 'name' => $accountName]);
- app('log')->warning('Not a valid source. Cant find it.', $validTypes);
+ Log::warning('Not a valid source. Cant find it.', $validTypes);
return false;
}
$this->setSource($search);
- app('log')->debug('Valid source account!');
+ Log::debug('Valid source account!');
return true;
}
diff --git a/app/Validation/AccountValidator.php b/app/Validation/AccountValidator.php
index 44c9ad9d7b..0605b82bf0 100644
--- a/app/Validation/AccountValidator.php
+++ b/app/Validation/AccountValidator.php
@@ -36,6 +36,7 @@ use FireflyIII\Validation\Account\OBValidation;
use FireflyIII\Validation\Account\ReconciliationValidation;
use FireflyIII\Validation\Account\TransferValidation;
use FireflyIII\Validation\Account\WithdrawalValidation;
+use Illuminate\Support\Facades\Log;
/**
* Class AccountValidator
@@ -80,10 +81,10 @@ class AccountValidator
public function setSource(?Account $account): void
{
if (!$account instanceof Account) {
- app('log')->debug('AccountValidator source is set to NULL');
+ Log::debug('AccountValidator source is set to NULL');
}
if ($account instanceof Account) {
- app('log')->debug(sprintf('AccountValidator source is set to #%d: "%s" (%s)', $account->id, $account->name, $account->accountType?->type));
+ Log::debug(sprintf('AccountValidator source is set to #%d: "%s" (%s)', $account->id, $account->name, $account->accountType?->type));
}
$this->source = $account;
}
@@ -91,17 +92,17 @@ class AccountValidator
public function setDestination(?Account $account): void
{
if (!$account instanceof Account) {
- app('log')->debug('AccountValidator destination is set to NULL');
+ Log::debug('AccountValidator destination is set to NULL');
}
if ($account instanceof Account) {
- app('log')->debug(sprintf('AccountValidator destination is set to #%d: "%s" (%s)', $account->id, $account->name, $account->accountType->type));
+ Log::debug(sprintf('AccountValidator destination is set to #%d: "%s" (%s)', $account->id, $account->name, $account->accountType->type));
}
$this->destination = $account;
}
public function setTransactionType(string $transactionType): void
{
- app('log')->debug(sprintf('Transaction type for validator is now "%s".', ucfirst($transactionType)));
+ Log::debug(sprintf('Transaction type for validator is now "%s".', ucfirst($transactionType)));
$this->transactionType = ucfirst($transactionType);
}
@@ -117,9 +118,9 @@ class AccountValidator
public function validateDestination(array $array): bool
{
- app('log')->debug('Now in AccountValidator::validateDestination()', $array);
+ Log::debug('Now in AccountValidator::validateDestination()', $array);
if (!$this->source instanceof Account) {
- app('log')->error('Source is NULL, always FALSE.');
+ Log::error('Source is NULL, always FALSE.');
$this->destError = 'No source account validation has taken place yet. Please do this first or overrule the object.';
return false;
@@ -128,7 +129,7 @@ class AccountValidator
switch ($this->transactionType) {
default:
$this->destError = sprintf('AccountValidator::validateDestination cannot handle "%s", so it will always return false.', $this->transactionType);
- app('log')->error(sprintf('AccountValidator::validateDestination cannot handle "%s", so it will always return false.', $this->transactionType));
+ Log::error(sprintf('AccountValidator::validateDestination cannot handle "%s", so it will always return false.', $this->transactionType));
$result = false;
@@ -170,11 +171,11 @@ class AccountValidator
public function validateSource(array $array): bool
{
- app('log')->debug('Now in AccountValidator::validateSource()', $array);
+ Log::debug('Now in AccountValidator::validateSource()', $array);
switch ($this->transactionType) {
default:
- app('log')->error(sprintf('AccountValidator::validateSource cannot handle "%s", so it will do a generic check.', $this->transactionType));
+ Log::error(sprintf('AccountValidator::validateSource cannot handle "%s", so it will do a generic check.', $this->transactionType));
$result = $this->validateGenericSource($array);
break;
@@ -205,7 +206,7 @@ class AccountValidator
break;
case TransactionTypeEnum::RECONCILIATION->value:
- app('log')->debug('Calling validateReconciliationSource');
+ Log::debug('Calling validateReconciliationSource');
$result = $this->validateReconciliationSource($array);
break;
@@ -216,17 +217,17 @@ class AccountValidator
protected function canCreateTypes(array $accountTypes): bool
{
- app('log')->debug('Can we create any of these types?', $accountTypes);
+ Log::debug('Can we create any of these types?', $accountTypes);
/** @var string $accountType */
foreach ($accountTypes as $accountType) {
if ($this->canCreateType($accountType)) {
- app('log')->debug(sprintf('YES, we can create a %s', $accountType));
+ Log::debug(sprintf('YES, we can create a %s', $accountType));
return true;
}
}
- app('log')->debug('NO, we cant create any of those.');
+ Log::debug('NO, we cant create any of those.');
return false;
}
@@ -250,8 +251,8 @@ class AccountValidator
*/
protected function findExistingAccount(array $validTypes, array $data, bool $inverse = false): ?Account
{
- app('log')->debug('Now in findExistingAccount', [$validTypes, $data]);
- app('log')->debug('The search will be reversed!');
+ Log::debug('Now in findExistingAccount', [$validTypes, $data]);
+ Log::debug('The search will be reversed!');
$accountId = array_key_exists('id', $data) ? $data['id'] : null;
$accountIban = array_key_exists('iban', $data) ? $data['iban'] : null;
$accountNumber = array_key_exists('number', $data) ? $data['number'] : null;
@@ -264,7 +265,7 @@ class AccountValidator
$check = in_array($accountType, $validTypes, true);
$check = $inverse ? !$check : $check; // reverse the validation check if necessary.
if (($first instanceof Account) && $check) {
- app('log')->debug(sprintf('ID: Found %s account #%d ("%s", IBAN "%s")', $first->accountType->type, $first->id, $first->name, $first->iban ?? 'no iban'));
+ Log::debug(sprintf('ID: Found %s account #%d ("%s", IBAN "%s")', $first->accountType->type, $first->id, $first->name, $first->iban ?? 'no iban'));
return $first;
}
@@ -277,7 +278,7 @@ class AccountValidator
$check = in_array($accountType, $validTypes, true);
$check = $inverse ? !$check : $check; // reverse the validation check if necessary.
if (($first instanceof Account) && $check) {
- app('log')->debug(sprintf('Iban: Found %s account #%d ("%s", IBAN "%s")', $first->accountType->type, $first->id, $first->name, $first->iban ?? 'no iban'));
+ Log::debug(sprintf('Iban: Found %s account #%d ("%s", IBAN "%s")', $first->accountType->type, $first->id, $first->name, $first->iban ?? 'no iban'));
return $first;
}
@@ -290,7 +291,7 @@ class AccountValidator
$check = in_array($accountType, $validTypes, true);
$check = $inverse ? !$check : $check; // reverse the validation check if necessary.
if (($first instanceof Account) && $check) {
- app('log')->debug(sprintf('Number: Found %s account #%d ("%s", IBAN "%s")', $first->accountType->type, $first->id, $first->name, $first->iban ?? 'no iban'));
+ Log::debug(sprintf('Number: Found %s account #%d ("%s", IBAN "%s")', $first->accountType->type, $first->id, $first->name, $first->iban ?? 'no iban'));
return $first;
}
@@ -300,12 +301,12 @@ class AccountValidator
if ('' !== (string) $accountName) {
$first = $this->accountRepository->findByName($accountName, $validTypes);
if ($first instanceof Account) {
- app('log')->debug(sprintf('Name: Found %s account #%d ("%s", IBAN "%s")', $first->accountType->type, $first->id, $first->name, $first->iban ?? 'no iban'));
+ Log::debug(sprintf('Name: Found %s account #%d ("%s", IBAN "%s")', $first->accountType->type, $first->id, $first->name, $first->iban ?? 'no iban'));
return $first;
}
}
- app('log')->debug('Found nothing in findExistingAccount()');
+ Log::debug('Found nothing in findExistingAccount()');
return null;
}
diff --git a/changelog.md b/changelog.md
index b37af58197..aecba981e0 100644
--- a/changelog.md
+++ b/changelog.md
@@ -3,7 +3,22 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
-## 6.4.0 - 2025-09-19
+## 6.4.1 - 2025-09-15
+
+### Fixed
+
+- Fixed a missing filter from [issue 10803](https://github.com/firefly-iii/firefly-iii/issues/10803).
+- #10891
+- #10920
+- #10921
+- #10833
+
+### API
+
+- #10908
+
+
+## 6.4.0 - 2025-09-14
### Added
diff --git a/composer.lock b/composer.lock
index 50beadca73..f3717d8cb4 100644
--- a/composer.lock
+++ b/composer.lock
@@ -324,16 +324,16 @@
},
{
"name": "dasprid/enum",
- "version": "1.0.6",
+ "version": "1.0.7",
"source": {
"type": "git",
"url": "https://github.com/DASPRiD/Enum.git",
- "reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90"
+ "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8dfd07c6d2cf31c8da90c53b83c026c7696dda90",
- "reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90",
+ "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce",
+ "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce",
"shasum": ""
},
"require": {
@@ -368,9 +368,9 @@
],
"support": {
"issues": "https://github.com/DASPRiD/Enum/issues",
- "source": "https://github.com/DASPRiD/Enum/tree/1.0.6"
+ "source": "https://github.com/DASPRiD/Enum/tree/1.0.7"
},
- "time": "2024-08-09T14:30:48+00:00"
+ "time": "2025-09-16T12:23:56+00:00"
},
{
"name": "defuse/php-encryption",
@@ -1878,16 +1878,16 @@
},
{
"name": "laravel/framework",
- "version": "v12.28.1",
+ "version": "v12.31.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "868c1f2d3dba4df6d21e3a8d818479f094cfd942"
+ "reference": "281b711710c245dd8275d73132e92635be3094df"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/868c1f2d3dba4df6d21e3a8d818479f094cfd942",
- "reference": "868c1f2d3dba4df6d21e3a8d818479f094cfd942",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/281b711710c245dd8275d73132e92635be3094df",
+ "reference": "281b711710c245dd8275d73132e92635be3094df",
"shasum": ""
},
"require": {
@@ -1915,6 +1915,7 @@
"monolog/monolog": "^3.0",
"nesbot/carbon": "^3.8.4",
"nunomaduro/termwind": "^2.0",
+ "phiki/phiki": "^2.0.0",
"php": "^8.2",
"psr/container": "^1.1.1|^2.0.1",
"psr/log": "^1.0|^2.0|^3.0",
@@ -2024,7 +2025,7 @@
"ext-pdo": "Required to use all database features.",
"ext-posix": "Required to use all features of the queue worker.",
"ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).",
- "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).",
+ "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).",
"filp/whoops": "Required for friendly error pages in development (^2.14.3).",
"laravel/tinker": "Required to use the tinker console command (^2.0).",
"league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).",
@@ -2093,7 +2094,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2025-09-04T14:58:12+00:00"
+ "time": "2025-09-23T15:33:04+00:00"
},
{
"name": "laravel/passport",
@@ -2173,16 +2174,16 @@
},
{
"name": "laravel/prompts",
- "version": "v0.3.6",
+ "version": "v0.3.7",
"source": {
"type": "git",
"url": "https://github.com/laravel/prompts.git",
- "reference": "86a8b692e8661d0fb308cec64f3d176821323077"
+ "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/prompts/zipball/86a8b692e8661d0fb308cec64f3d176821323077",
- "reference": "86a8b692e8661d0fb308cec64f3d176821323077",
+ "url": "https://api.github.com/repos/laravel/prompts/zipball/a1891d362714bc40c8d23b0b1d7090f022ea27cc",
+ "reference": "a1891d362714bc40c8d23b0b1d7090f022ea27cc",
"shasum": ""
},
"require": {
@@ -2199,8 +2200,8 @@
"illuminate/collections": "^10.0|^11.0|^12.0",
"mockery/mockery": "^1.5",
"pestphp/pest": "^2.3|^3.4",
- "phpstan/phpstan": "^1.11",
- "phpstan/phpstan-mockery": "^1.1"
+ "phpstan/phpstan": "^1.12.28",
+ "phpstan/phpstan-mockery": "^1.1.3"
},
"suggest": {
"ext-pcntl": "Required for the spinner to be animated."
@@ -2226,9 +2227,9 @@
"description": "Add beautiful and user-friendly forms to your command-line applications.",
"support": {
"issues": "https://github.com/laravel/prompts/issues",
- "source": "https://github.com/laravel/prompts/tree/v0.3.6"
+ "source": "https://github.com/laravel/prompts/tree/v0.3.7"
},
- "time": "2025-07-07T14:17:42+00:00"
+ "time": "2025-09-19T13:47:56+00:00"
},
{
"name": "laravel/sanctum",
@@ -2296,16 +2297,16 @@
},
{
"name": "laravel/serializable-closure",
- "version": "v2.0.4",
+ "version": "v2.0.5",
"source": {
"type": "git",
"url": "https://github.com/laravel/serializable-closure.git",
- "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841"
+ "reference": "3832547db6e0e2f8bb03d4093857b378c66eceed"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841",
- "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841",
+ "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/3832547db6e0e2f8bb03d4093857b378c66eceed",
+ "reference": "3832547db6e0e2f8bb03d4093857b378c66eceed",
"shasum": ""
},
"require": {
@@ -2353,7 +2354,7 @@
"issues": "https://github.com/laravel/serializable-closure/issues",
"source": "https://github.com/laravel/serializable-closure"
},
- "time": "2025-03-19T13:51:03+00:00"
+ "time": "2025-09-22T17:29:40+00:00"
},
{
"name": "laravel/slack-notification-channel",
@@ -4234,24 +4235,26 @@
},
{
"name": "paragonie/constant_time_encoding",
- "version": "v3.0.0",
+ "version": "v3.1.3",
"source": {
"type": "git",
"url": "https://github.com/paragonie/constant_time_encoding.git",
- "reference": "df1e7fde177501eee2037dd159cf04f5f301a512"
+ "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512",
- "reference": "df1e7fde177501eee2037dd159cf04f5f301a512",
+ "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77",
+ "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77",
"shasum": ""
},
"require": {
"php": "^8"
},
"require-dev": {
- "phpunit/phpunit": "^9",
- "vimeo/psalm": "^4|^5"
+ "infection/infection": "^0",
+ "nikic/php-fuzzer": "^0",
+ "phpunit/phpunit": "^9|^10|^11",
+ "vimeo/psalm": "^4|^5|^6"
},
"type": "library",
"autoload": {
@@ -4297,7 +4300,7 @@
"issues": "https://github.com/paragonie/constant_time_encoding/issues",
"source": "https://github.com/paragonie/constant_time_encoding"
},
- "time": "2024-05-08T12:36:18+00:00"
+ "time": "2025-09-24T15:06:41+00:00"
},
{
"name": "paragonie/random_compat",
@@ -4349,6 +4352,77 @@
},
"time": "2020-10-15T08:29:30+00:00"
},
+ {
+ "name": "phiki/phiki",
+ "version": "v2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phikiphp/phiki.git",
+ "reference": "160785c50c01077780ab217e5808f00ab8f05a13"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phikiphp/phiki/zipball/160785c50c01077780ab217e5808f00ab8f05a13",
+ "reference": "160785c50c01077780ab217e5808f00ab8f05a13",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "league/commonmark": "^2.5.3",
+ "php": "^8.2",
+ "psr/simple-cache": "^3.0"
+ },
+ "require-dev": {
+ "illuminate/support": "^11.45",
+ "laravel/pint": "^1.18.1",
+ "orchestra/testbench": "^9.15",
+ "pestphp/pest": "^3.5.1",
+ "phpstan/extension-installer": "^1.4.3",
+ "phpstan/phpstan": "^2.0",
+ "symfony/var-dumper": "^7.1.6"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Phiki\\Adapters\\Laravel\\PhikiServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Phiki\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ryan Chandler",
+ "email": "support@ryangjchandler.co.uk",
+ "homepage": "https://ryangjchandler.co.uk",
+ "role": "Developer"
+ }
+ ],
+ "description": "Syntax highlighting using TextMate grammars in PHP.",
+ "support": {
+ "issues": "https://github.com/phikiphp/phiki/issues",
+ "source": "https://github.com/phikiphp/phiki/tree/v2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sponsors/ryangjchandler",
+ "type": "github"
+ },
+ {
+ "url": "https://buymeacoffee.com/ryangjchandler",
+ "type": "other"
+ }
+ ],
+ "time": "2025-09-20T17:21:02+00:00"
+ },
{
"name": "php-http/client-common",
"version": "2.7.2",
@@ -4973,21 +5047,21 @@
},
{
"name": "pragmarx/google2fa-qrcode",
- "version": "v3.0.0",
+ "version": "v3.0.1",
"source": {
"type": "git",
"url": "https://github.com/antonioribeiro/google2fa-qrcode.git",
- "reference": "ce4d8a729b6c93741c607cfb2217acfffb5bf76b"
+ "reference": "c23ebcc3a50de0d1566016a6dd1486e183bb78e1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/antonioribeiro/google2fa-qrcode/zipball/ce4d8a729b6c93741c607cfb2217acfffb5bf76b",
- "reference": "ce4d8a729b6c93741c607cfb2217acfffb5bf76b",
+ "url": "https://api.github.com/repos/antonioribeiro/google2fa-qrcode/zipball/c23ebcc3a50de0d1566016a6dd1486e183bb78e1",
+ "reference": "c23ebcc3a50de0d1566016a6dd1486e183bb78e1",
"shasum": ""
},
"require": {
"php": ">=7.1",
- "pragmarx/google2fa": ">=4.0"
+ "pragmarx/google2fa": "^4.0|^5.0|^6.0|^7.0|^8.0"
},
"require-dev": {
"bacon/bacon-qr-code": "^2.0",
@@ -5034,9 +5108,9 @@
],
"support": {
"issues": "https://github.com/antonioribeiro/google2fa-qrcode/issues",
- "source": "https://github.com/antonioribeiro/google2fa-qrcode/tree/v3.0.0"
+ "source": "https://github.com/antonioribeiro/google2fa-qrcode/tree/v3.0.1"
},
- "time": "2021-08-15T12:53:48+00:00"
+ "time": "2025-09-19T23:02:26+00:00"
},
{
"name": "pragmarx/random",
@@ -10738,16 +10812,16 @@
},
{
"name": "larastan/larastan",
- "version": "v3.7.1",
+ "version": "v3.7.2",
"source": {
"type": "git",
"url": "https://github.com/larastan/larastan.git",
- "reference": "2e653fd19585a825e283b42f38378b21ae481cc7"
+ "reference": "a761859a7487bd7d0cb8b662a7538a234d5bb5ae"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/larastan/larastan/zipball/2e653fd19585a825e283b42f38378b21ae481cc7",
- "reference": "2e653fd19585a825e283b42f38378b21ae481cc7",
+ "url": "https://api.github.com/repos/larastan/larastan/zipball/a761859a7487bd7d0cb8b662a7538a234d5bb5ae",
+ "reference": "a761859a7487bd7d0cb8b662a7538a234d5bb5ae",
"shasum": ""
},
"require": {
@@ -10761,7 +10835,7 @@
"illuminate/pipeline": "^11.44.2 || ^12.4.1",
"illuminate/support": "^11.44.2 || ^12.4.1",
"php": "^8.2",
- "phpstan/phpstan": "^2.1.23"
+ "phpstan/phpstan": "^2.1.28"
},
"require-dev": {
"doctrine/coding-standard": "^13",
@@ -10815,7 +10889,7 @@
],
"support": {
"issues": "https://github.com/larastan/larastan/issues",
- "source": "https://github.com/larastan/larastan/tree/v3.7.1"
+ "source": "https://github.com/larastan/larastan/tree/v3.7.2"
},
"funding": [
{
@@ -10823,7 +10897,7 @@
"type": "github"
}
],
- "time": "2025-09-10T19:42:11+00:00"
+ "time": "2025-09-19T09:03:05+00:00"
},
{
"name": "laravel-json-api/testing",
@@ -11332,16 +11406,16 @@
},
{
"name": "phpstan/phpstan",
- "version": "2.1.25",
+ "version": "2.1.29",
"source": {
"type": "git",
- "url": "https://github.com/phpstan/phpstan.git",
- "reference": "4087d28bd252895874e174d65e26b2c202ed893a"
+ "url": "https://github.com/phpstan/phpstan-phar-composer-source.git",
+ "reference": "git"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/4087d28bd252895874e174d65e26b2c202ed893a",
- "reference": "4087d28bd252895874e174d65e26b2c202ed893a",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d618573eed4a1b6b75e37b2e0b65ac65c885d88e",
+ "reference": "d618573eed4a1b6b75e37b2e0b65ac65c885d88e",
"shasum": ""
},
"require": {
@@ -11386,7 +11460,7 @@
"type": "github"
}
],
- "time": "2025-09-12T14:26:42+00:00"
+ "time": "2025-09-25T06:58:18+00:00"
},
{
"name": "phpstan/phpstan-deprecation-rules",
@@ -11437,21 +11511,21 @@
},
{
"name": "phpstan/phpstan-strict-rules",
- "version": "2.0.6",
+ "version": "2.0.7",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-strict-rules.git",
- "reference": "f9f77efa9de31992a832ff77ea52eb42d675b094"
+ "reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/f9f77efa9de31992a832ff77ea52eb42d675b094",
- "reference": "f9f77efa9de31992a832ff77ea52eb42d675b094",
+ "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/d6211c46213d4181054b3d77b10a5c5cb0d59538",
+ "reference": "d6211c46213d4181054b3d77b10a5c5cb0d59538",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0",
- "phpstan/phpstan": "^2.0.4"
+ "phpstan/phpstan": "^2.1.29"
},
"require-dev": {
"php-parallel-lint/php-parallel-lint": "^1.2",
@@ -11479,22 +11553,22 @@
"description": "Extra strict and opinionated rules for PHPStan",
"support": {
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
- "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.6"
+ "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.7"
},
- "time": "2025-07-21T12:19:29+00:00"
+ "time": "2025-09-26T11:19:08+00:00"
},
{
"name": "phpunit/php-code-coverage",
- "version": "12.3.7",
+ "version": "12.4.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "bbede0f5593dad37af3be6a6f8e6ae1885e8a0a9"
+ "reference": "67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/bbede0f5593dad37af3be6a6f8e6ae1885e8a0a9",
- "reference": "bbede0f5593dad37af3be6a6f8e6ae1885e8a0a9",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c",
+ "reference": "67e8aed88f93d0e6e1cb7effe1a2dfc2fee6022c",
"shasum": ""
},
"require": {
@@ -11521,7 +11595,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "12.3.x-dev"
+ "dev-main": "12.4.x-dev"
}
},
"autoload": {
@@ -11550,7 +11624,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
- "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.3.7"
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.4.0"
},
"funding": [
{
@@ -11570,7 +11644,7 @@
"type": "tidelift"
}
],
- "time": "2025-09-10T09:59:06+00:00"
+ "time": "2025-09-24T13:44:41+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -11819,16 +11893,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "12.3.10",
+ "version": "12.3.14",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "0d401d0df2e3c1703be425ecdc2d04f5c095938d"
+ "reference": "13e9b2bea9327b094176147250d2c10319a10f5b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0d401d0df2e3c1703be425ecdc2d04f5c095938d",
- "reference": "0d401d0df2e3c1703be425ecdc2d04f5c095938d",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/13e9b2bea9327b094176147250d2c10319a10f5b",
+ "reference": "13e9b2bea9327b094176147250d2c10319a10f5b",
"shasum": ""
},
"require": {
@@ -11842,16 +11916,16 @@
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.3",
- "phpunit/php-code-coverage": "^12.3.7",
+ "phpunit/php-code-coverage": "^12.3.8",
"phpunit/php-file-iterator": "^6.0.0",
"phpunit/php-invoker": "^6.0.0",
"phpunit/php-text-template": "^5.0.0",
"phpunit/php-timer": "^8.0.0",
- "sebastian/cli-parser": "^4.0.0",
+ "sebastian/cli-parser": "^4.2.0",
"sebastian/comparator": "^7.1.3",
"sebastian/diff": "^7.0.0",
"sebastian/environment": "^8.0.3",
- "sebastian/exporter": "^7.0.0",
+ "sebastian/exporter": "^7.0.2",
"sebastian/global-state": "^8.0.2",
"sebastian/object-enumerator": "^7.0.0",
"sebastian/type": "^6.0.3",
@@ -11896,7 +11970,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/12.3.10"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/12.3.14"
},
"funding": [
{
@@ -11920,7 +11994,7 @@
"type": "tidelift"
}
],
- "time": "2025-09-11T10:35:19+00:00"
+ "time": "2025-09-24T06:34:27+00:00"
},
{
"name": "rector/rector",
@@ -11984,16 +12058,16 @@
},
{
"name": "sebastian/cli-parser",
- "version": "4.1.0",
+ "version": "4.2.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/cli-parser.git",
- "reference": "8fd93be538992d556aaa45c74570129448a42084"
+ "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/8fd93be538992d556aaa45c74570129448a42084",
- "reference": "8fd93be538992d556aaa45c74570129448a42084",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04",
+ "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04",
"shasum": ""
},
"require": {
@@ -12005,7 +12079,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "4.1-dev"
+ "dev-main": "4.2-dev"
}
},
"autoload": {
@@ -12029,7 +12103,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/cli-parser/issues",
"security": "https://github.com/sebastianbergmann/cli-parser/security/policy",
- "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.1.0"
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0"
},
"funding": [
{
@@ -12049,7 +12123,7 @@
"type": "tidelift"
}
],
- "time": "2025-09-13T14:16:18+00:00"
+ "time": "2025-09-14T09:36:45+00:00"
},
{
"name": "sebastian/comparator",
@@ -12346,16 +12420,16 @@
},
{
"name": "sebastian/exporter",
- "version": "7.0.0",
+ "version": "7.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "76432aafc58d50691a00d86d0632f1217a47b688"
+ "reference": "016951ae10980765e4e7aee491eb288c64e505b7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/76432aafc58d50691a00d86d0632f1217a47b688",
- "reference": "76432aafc58d50691a00d86d0632f1217a47b688",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7",
+ "reference": "016951ae10980765e4e7aee491eb288c64e505b7",
"shasum": ""
},
"require": {
@@ -12412,15 +12486,27 @@
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
"security": "https://github.com/sebastianbergmann/exporter/security/policy",
- "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.0"
+ "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
+ "type": "tidelift"
}
],
- "time": "2025-02-07T04:56:42+00:00"
+ "time": "2025-09-24T06:16:11+00:00"
},
{
"name": "sebastian/global-state",
diff --git a/config/firefly.php b/config/firefly.php
index c2af8616ae..da4d0d6233 100644
--- a/config/firefly.php
+++ b/config/firefly.php
@@ -78,10 +78,10 @@ return [
'running_balance_column' => env('USE_RUNNING_BALANCE', false),
// see cer.php for exchange rates feature flag.
],
- 'version' => '6.4.0',
- 'build_time' => 1757781366,
+ 'version' => 'develop/2025-09-27',
+ 'build_time' => 1758945787,
'api_version' => '2.1.0', // field is no longer used.
- 'db_version' => 26,
+ 'db_version' => 27,
// generic settings
'maxUploadSize' => 1073741824, // 1 GB
diff --git a/database/migrations/2024_11_30_075826_multi_piggy.php b/database/migrations/2024_11_30_075826_multi_piggy.php
index 19442cd051..a89f29440d 100644
--- a/database/migrations/2024_11_30_075826_multi_piggy.php
+++ b/database/migrations/2024_11_30_075826_multi_piggy.php
@@ -140,7 +140,7 @@ return new class () extends Migration {
$table->dropColumn('transaction_currency_id');
// 2. make column non-nullable.
- $table->unsignedInteger('account_id')->change();
+ $table->unsignedInteger('account_id')->nullable()->change();
// 5. add new index
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
diff --git a/database/migrations/2025_09_25_175248_create_period_statistics.php b/database/migrations/2025_09_25_175248_create_period_statistics.php
new file mode 100644
index 0000000000..0cda62b0ef
--- /dev/null
+++ b/database/migrations/2025_09_25_175248_create_period_statistics.php
@@ -0,0 +1,51 @@
+id();
+ $table->timestamps();
+
+ // reference to user group id.
+ $table->bigInteger('user_group_id', false, true);
+
+ $table->integer('primary_statable_id', false, true)->nullable();
+ $table->string('primary_statable_type', 255)->nullable();
+
+ $table->integer('secondary_statable_id', false, true)->nullable();
+ $table->string('secondary_statable_type', 255)->nullable();
+
+ $table->integer('tertiary_statable_id', false, true)->nullable();
+ $table->string('tertiary_statable_type', 255)->nullable();
+
+ $table->integer('transaction_currency_id', false, true);
+ $table->foreign('transaction_currency_id')->references('id')->on('transaction_currencies')->onDelete('cascade');
+
+ $table->dateTime('start')->nullable();
+ $table->string('start_tz', 50)->nullable();
+ $table->dateTime('end')->nullable();
+ $table->string('end_tz', 50)->nullable();
+ $table->string('type',255);
+ $table->integer('count', false, true)->default(0);
+ $table->decimal('amount', 32, 12);
+ $table->foreign('user_group_id')->references('id')->on('user_groups')->onDelete('cascade');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('period_statistics');
+ }
+};
diff --git a/package-lock.json b/package-lock.json
index 211a5d750c..09bb10c0a6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1693,9 +1693,9 @@
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
- "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz",
+ "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==",
"cpu": [
"ppc64"
],
@@ -1710,9 +1710,9 @@
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz",
- "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz",
+ "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==",
"cpu": [
"arm"
],
@@ -1727,9 +1727,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz",
- "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz",
+ "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==",
"cpu": [
"arm64"
],
@@ -1744,9 +1744,9 @@
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz",
- "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz",
+ "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==",
"cpu": [
"x64"
],
@@ -1761,9 +1761,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz",
- "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz",
+ "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==",
"cpu": [
"arm64"
],
@@ -1778,9 +1778,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz",
- "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz",
+ "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==",
"cpu": [
"x64"
],
@@ -1795,9 +1795,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz",
- "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz",
+ "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==",
"cpu": [
"arm64"
],
@@ -1812,9 +1812,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz",
- "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz",
+ "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==",
"cpu": [
"x64"
],
@@ -1829,9 +1829,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz",
- "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz",
+ "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==",
"cpu": [
"arm"
],
@@ -1846,9 +1846,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz",
- "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz",
+ "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==",
"cpu": [
"arm64"
],
@@ -1863,9 +1863,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz",
- "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz",
+ "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==",
"cpu": [
"ia32"
],
@@ -1880,9 +1880,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz",
- "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz",
+ "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==",
"cpu": [
"loong64"
],
@@ -1897,9 +1897,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz",
- "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz",
+ "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==",
"cpu": [
"mips64el"
],
@@ -1914,9 +1914,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz",
- "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz",
+ "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==",
"cpu": [
"ppc64"
],
@@ -1931,9 +1931,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz",
- "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz",
+ "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==",
"cpu": [
"riscv64"
],
@@ -1948,9 +1948,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz",
- "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz",
+ "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==",
"cpu": [
"s390x"
],
@@ -1965,9 +1965,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz",
- "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz",
+ "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==",
"cpu": [
"x64"
],
@@ -1982,9 +1982,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz",
- "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz",
+ "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==",
"cpu": [
"arm64"
],
@@ -1999,9 +1999,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz",
- "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz",
+ "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==",
"cpu": [
"x64"
],
@@ -2016,9 +2016,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz",
- "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz",
+ "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==",
"cpu": [
"arm64"
],
@@ -2033,9 +2033,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz",
- "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz",
+ "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==",
"cpu": [
"x64"
],
@@ -2050,9 +2050,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz",
- "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz",
+ "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==",
"cpu": [
"arm64"
],
@@ -2067,9 +2067,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz",
- "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz",
+ "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==",
"cpu": [
"x64"
],
@@ -2084,9 +2084,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz",
- "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz",
+ "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==",
"cpu": [
"arm64"
],
@@ -2101,9 +2101,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz",
- "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz",
+ "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==",
"cpu": [
"ia32"
],
@@ -2118,9 +2118,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
- "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz",
+ "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==",
"cpu": [
"x64"
],
@@ -2589,9 +2589,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz",
- "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.2.tgz",
+ "integrity": "sha512-o3pcKzJgSGt4d74lSZ+OCnHwkKBeAbFDmbEm5gg70eA8VkyCuC/zV9TwBnmw6VjDlRdF4Pshfb+WE9E6XY1PoQ==",
"cpu": [
"arm"
],
@@ -2603,9 +2603,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz",
- "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.2.tgz",
+ "integrity": "sha512-cqFSWO5tX2vhC9hJTK8WAiPIm4Q8q/cU8j2HQA0L3E1uXvBYbOZMhE2oFL8n2pKB5sOCHY6bBuHaRwG7TkfJyw==",
"cpu": [
"arm64"
],
@@ -2617,9 +2617,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz",
- "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.2.tgz",
+ "integrity": "sha512-vngduywkkv8Fkh3wIZf5nFPXzWsNsVu1kvtLETWxTFf/5opZmflgVSeLgdHR56RQh71xhPhWoOkEBvbehwTlVA==",
"cpu": [
"arm64"
],
@@ -2631,9 +2631,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz",
- "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.2.tgz",
+ "integrity": "sha512-h11KikYrUCYTrDj6h939hhMNlqU2fo/X4NB0OZcys3fya49o1hmFaczAiJWVAFgrM1NCP6RrO7lQKeVYSKBPSQ==",
"cpu": [
"x64"
],
@@ -2645,9 +2645,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz",
- "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.2.tgz",
+ "integrity": "sha512-/eg4CI61ZUkLXxMHyVlmlGrSQZ34xqWlZNW43IAU4RmdzWEx0mQJ2mN/Cx4IHLVZFL6UBGAh+/GXhgvGb+nVxw==",
"cpu": [
"arm64"
],
@@ -2659,9 +2659,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz",
- "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.2.tgz",
+ "integrity": "sha512-QOWgFH5X9+p+S1NAfOqc0z8qEpJIoUHf7OWjNUGOeW18Mx22lAUOiA9b6r2/vpzLdfxi/f+VWsYjUOMCcYh0Ng==",
"cpu": [
"x64"
],
@@ -2673,9 +2673,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz",
- "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.2.tgz",
+ "integrity": "sha512-kDWSPafToDd8LcBYd1t5jw7bD5Ojcu12S3uT372e5HKPzQt532vW+rGFFOaiR0opxePyUkHrwz8iWYEyH1IIQA==",
"cpu": [
"arm"
],
@@ -2687,9 +2687,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz",
- "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.2.tgz",
+ "integrity": "sha512-gKm7Mk9wCv6/rkzwCiUC4KnevYhlf8ztBrDRT9g/u//1fZLapSRc+eDZj2Eu2wpJ+0RzUKgtNijnVIB4ZxyL+w==",
"cpu": [
"arm"
],
@@ -2701,9 +2701,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz",
- "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.2.tgz",
+ "integrity": "sha512-66lA8vnj5mB/rtDNwPgrrKUOtCLVQypkyDa2gMfOefXK6rcZAxKLO9Fy3GkW8VkPnENv9hBkNOFfGLf6rNKGUg==",
"cpu": [
"arm64"
],
@@ -2715,9 +2715,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz",
- "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.2.tgz",
+ "integrity": "sha512-s+OPucLNdJHvuZHuIz2WwncJ+SfWHFEmlC5nKMUgAelUeBUnlB4wt7rXWiyG4Zn07uY2Dd+SGyVa9oyLkVGOjA==",
"cpu": [
"arm64"
],
@@ -2728,10 +2728,10 @@
"linux"
]
},
- "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz",
- "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==",
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.2.tgz",
+ "integrity": "sha512-8wTRM3+gVMDLLDdaT6tKmOE3lJyRy9NpJUS/ZRWmLCmOPIJhVyXwjBo+XbrrwtV33Em1/eCTd5TuGJm4+DmYjw==",
"cpu": [
"loong64"
],
@@ -2743,9 +2743,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz",
- "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.2.tgz",
+ "integrity": "sha512-6yqEfgJ1anIeuP2P/zhtfBlDpXUb80t8DpbYwXQ3bQd95JMvUaqiX+fKqYqUwZXqdJDd8xdilNtsHM2N0cFm6A==",
"cpu": [
"ppc64"
],
@@ -2757,9 +2757,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz",
- "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.2.tgz",
+ "integrity": "sha512-sshYUiYVSEI2B6dp4jMncwxbrUqRdNApF2c3bhtLAU0qA8Lrri0p0NauOsTWh3yCCCDyBOjESHMExonp7Nzc0w==",
"cpu": [
"riscv64"
],
@@ -2771,9 +2771,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz",
- "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.2.tgz",
+ "integrity": "sha512-duBLgd+3pqC4MMwBrKkFxaZerUxZcYApQVC5SdbF5/e/589GwVvlRUnyqMFbM8iUSb1BaoX/3fRL7hB9m2Pj8Q==",
"cpu": [
"riscv64"
],
@@ -2785,9 +2785,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz",
- "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.2.tgz",
+ "integrity": "sha512-tzhYJJidDUVGMgVyE+PmxENPHlvvqm1KILjjZhB8/xHYqAGeizh3GBGf9u6WdJpZrz1aCpIIHG0LgJgH9rVjHQ==",
"cpu": [
"s390x"
],
@@ -2799,9 +2799,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz",
- "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.2.tgz",
+ "integrity": "sha512-opH8GSUuVcCSSyHHcl5hELrmnk4waZoVpgn/4FDao9iyE4WpQhyWJ5ryl5M3ocp4qkRuHfyXnGqg8M9oKCEKRA==",
"cpu": [
"x64"
],
@@ -2813,9 +2813,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz",
- "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.2.tgz",
+ "integrity": "sha512-LSeBHnGli1pPKVJ79ZVJgeZWWZXkEe/5o8kcn23M8eMKCUANejchJbF/JqzM4RRjOJfNRhKJk8FuqL1GKjF5oQ==",
"cpu": [
"x64"
],
@@ -2827,9 +2827,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz",
- "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.2.tgz",
+ "integrity": "sha512-uPj7MQ6/s+/GOpolavm6BPo+6CbhbKYyZHUDvZ/SmJM7pfDBgdGisFX3bY/CBDMg2ZO4utfhlApkSfZ92yXw7Q==",
"cpu": [
"arm64"
],
@@ -2841,9 +2841,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz",
- "integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.2.tgz",
+ "integrity": "sha512-Z9MUCrSgIaUeeHAiNkm3cQyst2UhzjPraR3gYYfOjAuZI7tcFRTOD+4cHLPoS/3qinchth+V56vtqz1Tv+6KPA==",
"cpu": [
"arm64"
],
@@ -2855,9 +2855,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz",
- "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.2.tgz",
+ "integrity": "sha512-+GnYBmpjldD3XQd+HMejo+0gJGwYIOfFeoBQv32xF/RUIvccUz20/V6Otdv+57NE70D5pa8W/jVGDoGq0oON4A==",
"cpu": [
"ia32"
],
@@ -2868,10 +2868,24 @@
"win32"
]
},
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.2.tgz",
+ "integrity": "sha512-ApXFKluSB6kDQkAqZOKXBjiaqdF1BlKi+/eqnYe9Ee7U2K3pUDKsIyr8EYm/QDHTJIM+4X+lI0gJc3TTRhd+dA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz",
- "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.2.tgz",
+ "integrity": "sha512-ARz+Bs8kY6FtitYM96PqPEVvPXqEZmPZsSkXvyX19YzDqkCaIlhCieLLMI5hxO9SRZ2XtCtm8wxhy0iJ2jxNfw==",
"cpu": [
"x64"
],
@@ -3159,13 +3173,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "24.3.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.3.tgz",
- "integrity": "sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw==",
+ "version": "24.5.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz",
+ "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "undici-types": "~7.10.0"
+ "undici-types": "~7.12.0"
}
},
"node_modules/@types/node-forge": {
@@ -3267,57 +3281,57 @@
}
},
"node_modules/@vue/compiler-core": {
- "version": "3.5.21",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.21.tgz",
- "integrity": "sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==",
+ "version": "3.5.22",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.22.tgz",
+ "integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.28.3",
- "@vue/shared": "3.5.21",
+ "@babel/parser": "^7.28.4",
+ "@vue/shared": "3.5.22",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
- "version": "3.5.21",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.21.tgz",
- "integrity": "sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==",
+ "version": "3.5.22",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz",
+ "integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vue/compiler-core": "3.5.21",
- "@vue/shared": "3.5.21"
+ "@vue/compiler-core": "3.5.22",
+ "@vue/shared": "3.5.22"
}
},
"node_modules/@vue/compiler-sfc": {
- "version": "3.5.21",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.21.tgz",
- "integrity": "sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==",
+ "version": "3.5.22",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz",
+ "integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.28.3",
- "@vue/compiler-core": "3.5.21",
- "@vue/compiler-dom": "3.5.21",
- "@vue/compiler-ssr": "3.5.21",
- "@vue/shared": "3.5.21",
+ "@babel/parser": "^7.28.4",
+ "@vue/compiler-core": "3.5.22",
+ "@vue/compiler-dom": "3.5.22",
+ "@vue/compiler-ssr": "3.5.22",
+ "@vue/shared": "3.5.22",
"estree-walker": "^2.0.2",
- "magic-string": "^0.30.18",
+ "magic-string": "^0.30.19",
"postcss": "^8.5.6",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-ssr": {
- "version": "3.5.21",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.21.tgz",
- "integrity": "sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==",
+ "version": "3.5.22",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz",
+ "integrity": "sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vue/compiler-dom": "3.5.21",
- "@vue/shared": "3.5.21"
+ "@vue/compiler-dom": "3.5.22",
+ "@vue/shared": "3.5.22"
}
},
"node_modules/@vue/component-compiler-utils": {
@@ -3399,9 +3413,9 @@
"license": "MIT"
},
"node_modules/@vue/shared": {
- "version": "3.5.21",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.21.tgz",
- "integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==",
+ "version": "3.5.22",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.22.tgz",
+ "integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==",
"dev": true,
"license": "MIT"
},
@@ -3949,9 +3963,9 @@
}
},
"node_modules/axios": {
- "version": "1.12.1",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.1.tgz",
- "integrity": "sha512-Kn4kbSXpkFHCGE6rBFNwIv0GQs4AvDT80jlveJDKFxjbTYMUeB4QtsdPCv6H8Cm19Je7IU6VFtRl2zWZI0rudQ==",
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
+ "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4061,9 +4075,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.8.3",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.3.tgz",
- "integrity": "sha512-mcE+Wr2CAhHNWxXN/DdTI+n4gsPc5QpXpWnyCQWiQYIYZX+ZMJ8juXZgjRa/0/YPJo/NSsgW15/YgmI4nbysYw==",
+ "version": "2.8.7",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.7.tgz",
+ "integrity": "sha512-bxxN2M3a4d1CRoQC//IqsR5XrLh0IJ8TCv2x6Y9N0nckNz/rTjZB3//GGscZziZOxmjP55rzxg/ze7usFI9FqQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -4315,25 +4329,24 @@
}
},
"node_modules/browserify-sign": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz",
- "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==",
+ "version": "4.2.5",
+ "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz",
+ "integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==",
"dev": true,
"license": "ISC",
"dependencies": {
- "bn.js": "^5.2.1",
- "browserify-rsa": "^4.1.0",
+ "bn.js": "^5.2.2",
+ "browserify-rsa": "^4.1.1",
"create-hash": "^1.2.0",
"create-hmac": "^1.1.7",
- "elliptic": "^6.5.5",
- "hash-base": "~3.0",
+ "elliptic": "^6.6.1",
"inherits": "^2.0.4",
- "parse-asn1": "^5.1.7",
+ "parse-asn1": "^5.1.9",
"readable-stream": "^2.3.8",
"safe-buffer": "^5.2.1"
},
"engines": {
- "node": ">= 0.12"
+ "node": ">= 0.10"
}
},
"node_modules/browserify-zlib": {
@@ -4347,9 +4360,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.26.0",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.0.tgz",
- "integrity": "sha512-P9go2WrP9FiPwLv3zqRD/Uoxo0RSHjzFCiQz7d4vbmwNqQFo9T9WCeP/Qn5EbcKQY6DBbkxEXNcpJOmncNrb7A==",
+ "version": "4.26.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz",
+ "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==",
"dev": true,
"funding": [
{
@@ -4367,7 +4380,7 @@
],
"license": "MIT",
"dependencies": {
- "baseline-browser-mapping": "^2.8.2",
+ "baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
"electron-to-chromium": "^1.5.218",
"node-releases": "^2.0.21",
@@ -4508,9 +4521,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001741",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz",
- "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==",
+ "version": "1.0.30001745",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz",
+ "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==",
"dev": true,
"funding": [
{
@@ -4638,14 +4651,15 @@
}
},
"node_modules/cipher-base": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz",
- "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==",
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz",
+ "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==",
"dev": true,
"license": "MIT",
"dependencies": {
"inherits": "^2.0.4",
- "safe-buffer": "^5.2.1"
+ "safe-buffer": "^5.2.1",
+ "to-buffer": "^1.2.2"
},
"engines": {
"node": ">= 0.10"
@@ -5375,9 +5389,9 @@
"license": "MIT"
},
"node_modules/debug": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
- "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5722,9 +5736,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.218",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.218.tgz",
- "integrity": "sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==",
+ "version": "1.5.224",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.224.tgz",
+ "integrity": "sha512-kWAoUu/bwzvnhpdZSIc6KUyvkI1rbRXMT0Eq8pKReyOyaPZcctMli+EgvcN1PAvwVc7Tdo4Fxi2PsLNDU05mdg==",
"dev": true,
"license": "ISC"
},
@@ -5819,9 +5833,9 @@
}
},
"node_modules/error-ex": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
- "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
+ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5885,9 +5899,9 @@
}
},
"node_modules/esbuild": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
- "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz",
+ "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -5898,32 +5912,32 @@
"node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.25.9",
- "@esbuild/android-arm": "0.25.9",
- "@esbuild/android-arm64": "0.25.9",
- "@esbuild/android-x64": "0.25.9",
- "@esbuild/darwin-arm64": "0.25.9",
- "@esbuild/darwin-x64": "0.25.9",
- "@esbuild/freebsd-arm64": "0.25.9",
- "@esbuild/freebsd-x64": "0.25.9",
- "@esbuild/linux-arm": "0.25.9",
- "@esbuild/linux-arm64": "0.25.9",
- "@esbuild/linux-ia32": "0.25.9",
- "@esbuild/linux-loong64": "0.25.9",
- "@esbuild/linux-mips64el": "0.25.9",
- "@esbuild/linux-ppc64": "0.25.9",
- "@esbuild/linux-riscv64": "0.25.9",
- "@esbuild/linux-s390x": "0.25.9",
- "@esbuild/linux-x64": "0.25.9",
- "@esbuild/netbsd-arm64": "0.25.9",
- "@esbuild/netbsd-x64": "0.25.9",
- "@esbuild/openbsd-arm64": "0.25.9",
- "@esbuild/openbsd-x64": "0.25.9",
- "@esbuild/openharmony-arm64": "0.25.9",
- "@esbuild/sunos-x64": "0.25.9",
- "@esbuild/win32-arm64": "0.25.9",
- "@esbuild/win32-ia32": "0.25.9",
- "@esbuild/win32-x64": "0.25.9"
+ "@esbuild/aix-ppc64": "0.25.10",
+ "@esbuild/android-arm": "0.25.10",
+ "@esbuild/android-arm64": "0.25.10",
+ "@esbuild/android-x64": "0.25.10",
+ "@esbuild/darwin-arm64": "0.25.10",
+ "@esbuild/darwin-x64": "0.25.10",
+ "@esbuild/freebsd-arm64": "0.25.10",
+ "@esbuild/freebsd-x64": "0.25.10",
+ "@esbuild/linux-arm": "0.25.10",
+ "@esbuild/linux-arm64": "0.25.10",
+ "@esbuild/linux-ia32": "0.25.10",
+ "@esbuild/linux-loong64": "0.25.10",
+ "@esbuild/linux-mips64el": "0.25.10",
+ "@esbuild/linux-ppc64": "0.25.10",
+ "@esbuild/linux-riscv64": "0.25.10",
+ "@esbuild/linux-s390x": "0.25.10",
+ "@esbuild/linux-x64": "0.25.10",
+ "@esbuild/netbsd-arm64": "0.25.10",
+ "@esbuild/netbsd-x64": "0.25.10",
+ "@esbuild/openbsd-arm64": "0.25.10",
+ "@esbuild/openbsd-x64": "0.25.10",
+ "@esbuild/openharmony-arm64": "0.25.10",
+ "@esbuild/sunos-x64": "0.25.10",
+ "@esbuild/win32-arm64": "0.25.10",
+ "@esbuild/win32-ia32": "0.25.10",
+ "@esbuild/win32-x64": "0.25.10"
}
},
"node_modules/escalade": {
@@ -8747,17 +8761,16 @@
}
},
"node_modules/parse-asn1": {
- "version": "5.1.7",
- "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz",
- "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==",
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz",
+ "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==",
"dev": true,
"license": "ISC",
"dependencies": {
"asn1.js": "^4.10.1",
"browserify-aes": "^1.2.0",
"evp_bytestokey": "^1.0.3",
- "hash-base": "~3.0",
- "pbkdf2": "^3.1.2",
+ "pbkdf2": "^3.1.5",
"safe-buffer": "^5.2.1"
},
"engines": {
@@ -8923,55 +8936,21 @@
}
},
"node_modules/pbkdf2": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.3.tgz",
- "integrity": "sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz",
+ "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "create-hash": "~1.1.3",
+ "create-hash": "^1.2.0",
"create-hmac": "^1.1.7",
- "ripemd160": "=2.0.1",
+ "ripemd160": "^2.0.3",
"safe-buffer": "^5.2.1",
- "sha.js": "^2.4.11",
- "to-buffer": "^1.2.0"
+ "sha.js": "^2.4.12",
+ "to-buffer": "^1.2.1"
},
"engines": {
- "node": ">=0.12"
- }
- },
- "node_modules/pbkdf2/node_modules/create-hash": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz",
- "integrity": "sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cipher-base": "^1.0.1",
- "inherits": "^2.0.1",
- "ripemd160": "^2.0.0",
- "sha.js": "^2.4.0"
- }
- },
- "node_modules/pbkdf2/node_modules/hash-base": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz",
- "integrity": "sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "inherits": "^2.0.1"
- }
- },
- "node_modules/pbkdf2/node_modules/ripemd160": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz",
- "integrity": "sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "hash-base": "^2.0.0",
- "inherits": "^2.0.1"
+ "node": ">= 0.10"
}
},
"node_modules/picocolors": {
@@ -9947,16 +9926,16 @@
}
},
"node_modules/regexpu-core": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.3.1.tgz",
- "integrity": "sha512-DzcswPr252wEr7Qz8AyAVbfyBDKLoYp6eRA1We2Fa9qirRFSdtkP5sHr3yglDKy2BbA0fd2T+j/CUSKes3FeVQ==",
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz",
+ "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==",
"dev": true,
"license": "MIT",
"dependencies": {
"regenerate": "^1.4.2",
"regenerate-unicode-properties": "^10.2.2",
"regjsgen": "^0.8.0",
- "regjsparser": "^0.12.0",
+ "regjsparser": "^0.13.0",
"unicode-match-property-ecmascript": "^2.0.0",
"unicode-match-property-value-ecmascript": "^2.2.1"
},
@@ -9972,31 +9951,18 @@
"license": "MIT"
},
"node_modules/regjsparser": {
- "version": "0.12.0",
- "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz",
- "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==",
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz",
+ "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
- "jsesc": "~3.0.2"
+ "jsesc": "~3.1.0"
},
"bin": {
"regjsparser": "bin/parser"
}
},
- "node_modules/regjsparser/node_modules/jsesc": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
- "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "jsesc": "bin/jsesc"
- },
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/relateurl": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
@@ -10134,20 +10100,39 @@
}
},
"node_modules/ripemd160": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
- "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz",
+ "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "hash-base": "^3.0.0",
- "inherits": "^2.0.1"
+ "hash-base": "^3.1.2",
+ "inherits": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/ripemd160/node_modules/hash-base": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz",
+ "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.4",
+ "readable-stream": "^2.3.8",
+ "safe-buffer": "^5.2.1",
+ "to-buffer": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
}
},
"node_modules/rollup": {
- "version": "4.50.1",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz",
- "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==",
+ "version": "4.52.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.2.tgz",
+ "integrity": "sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10161,27 +10146,28 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.50.1",
- "@rollup/rollup-android-arm64": "4.50.1",
- "@rollup/rollup-darwin-arm64": "4.50.1",
- "@rollup/rollup-darwin-x64": "4.50.1",
- "@rollup/rollup-freebsd-arm64": "4.50.1",
- "@rollup/rollup-freebsd-x64": "4.50.1",
- "@rollup/rollup-linux-arm-gnueabihf": "4.50.1",
- "@rollup/rollup-linux-arm-musleabihf": "4.50.1",
- "@rollup/rollup-linux-arm64-gnu": "4.50.1",
- "@rollup/rollup-linux-arm64-musl": "4.50.1",
- "@rollup/rollup-linux-loongarch64-gnu": "4.50.1",
- "@rollup/rollup-linux-ppc64-gnu": "4.50.1",
- "@rollup/rollup-linux-riscv64-gnu": "4.50.1",
- "@rollup/rollup-linux-riscv64-musl": "4.50.1",
- "@rollup/rollup-linux-s390x-gnu": "4.50.1",
- "@rollup/rollup-linux-x64-gnu": "4.50.1",
- "@rollup/rollup-linux-x64-musl": "4.50.1",
- "@rollup/rollup-openharmony-arm64": "4.50.1",
- "@rollup/rollup-win32-arm64-msvc": "4.50.1",
- "@rollup/rollup-win32-ia32-msvc": "4.50.1",
- "@rollup/rollup-win32-x64-msvc": "4.50.1",
+ "@rollup/rollup-android-arm-eabi": "4.52.2",
+ "@rollup/rollup-android-arm64": "4.52.2",
+ "@rollup/rollup-darwin-arm64": "4.52.2",
+ "@rollup/rollup-darwin-x64": "4.52.2",
+ "@rollup/rollup-freebsd-arm64": "4.52.2",
+ "@rollup/rollup-freebsd-x64": "4.52.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.52.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.52.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.52.2",
+ "@rollup/rollup-linux-arm64-musl": "4.52.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.52.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.52.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.52.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.52.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.52.2",
+ "@rollup/rollup-linux-x64-gnu": "4.52.2",
+ "@rollup/rollup-linux-x64-musl": "4.52.2",
+ "@rollup/rollup-openharmony-arm64": "4.52.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.52.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.52.2",
+ "@rollup/rollup-win32-x64-gnu": "4.52.2",
+ "@rollup/rollup-win32-x64-msvc": "4.52.2",
"fsevents": "~2.3.2"
}
},
@@ -10237,9 +10223,9 @@
"license": "MIT"
},
"node_modules/sass": {
- "version": "1.92.1",
- "resolved": "https://registry.npmjs.org/sass/-/sass-1.92.1.tgz",
- "integrity": "sha512-ffmsdbwqb3XeyR8jJR6KelIXARM9bFQe8A6Q3W4Klmwy5Ckd5gz7jgUNHo4UOqutU5Sk1DtKLbpDP0nLCg1xqQ==",
+ "version": "1.93.2",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz",
+ "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11248,9 +11234,9 @@
"license": "MIT"
},
"node_modules/to-buffer": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz",
- "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==",
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
+ "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11357,9 +11343,9 @@
}
},
"node_modules/undici-types": {
- "version": "7.10.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
- "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
+ "version": "7.12.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz",
+ "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==",
"dev": true,
"license": "MIT"
},
@@ -11398,9 +11384,9 @@
}
},
"node_modules/unicode-property-aliases-ecmascript": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz",
- "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz",
+ "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -11554,9 +11540,9 @@
}
},
"node_modules/vite": {
- "version": "7.1.5",
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz",
- "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==",
+ "version": "7.1.7",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz",
+ "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/resources/assets/v1/src/locales/cs.json b/resources/assets/v1/src/locales/cs.json
index 66c89e114d..f1040e3ae7 100644
--- a/resources/assets/v1/src/locales/cs.json
+++ b/resources/assets/v1/src/locales/cs.json
@@ -53,7 +53,7 @@
"external_url": "Extern\u00ed URL adresa",
"update_transaction": "Aktualizovat transakci",
"after_update_create_another": "Po aktualizaci se vr\u00e1tit sem pro pokra\u010dov\u00e1n\u00ed v \u00faprav\u00e1ch.",
- "store_as_new": "Store as a new transaction instead of updating.",
+ "store_as_new": "Vytvo\u0159it novou transakci m\u00edsto aktualizov\u00e1n\u00ed t\u00e9 sou\u010dasn\u00e9.",
"split_title_help": "Pokud vytvo\u0159\u00edte roz\u00fa\u010dtov\u00e1n\u00ed, je t\u0159eba, aby zde byl celkov\u00fd popis pro v\u0161echna roz\u00fa\u010dtov\u00e1n\u00ed dan\u00e9 transakce.",
"none_in_select_list": "(\u017e\u00e1dn\u00e9)",
"no_piggy_bank": "(\u017e\u00e1dn\u00e1 pokladni\u010dka)",
diff --git a/resources/assets/v1/src/locales/it.json b/resources/assets/v1/src/locales/it.json
index dc449681e6..e297e8cb29 100644
--- a/resources/assets/v1/src/locales/it.json
+++ b/resources/assets/v1/src/locales/it.json
@@ -2,9 +2,9 @@
"firefly": {
"administrations_page_title": "Amministrazioni finanziarie",
"administrations_index_menu": "Amministrazioni finanziarie",
- "expires_at": "Expires at",
- "temp_administrations_introduction": "Firefly III will soon get the ability to manage multiple financial administrations. Right now, you only have the one. You can set the title of this administration and its primary currency. This replaces the previous setting where you would set your \"default currency\". This setting is now tied to the financial administration and can be different per administration.",
- "administration_currency_form_help": "It may take a long time for the page to load if you change the primary currency because transaction may need to be converted to your (new) primary currency.",
+ "expires_at": "Scade il",
+ "temp_administrations_introduction": "Firefly III avr\u00e0 presto la possibilit\u00e0 di gestire pi\u00f9 amministrazioni finanziarie. Al momento, ne hai solo una. Puoi impostare il titolo di questa amministrazione e la sua valuta principale. Questa impostazione sostituisce la precedente, che prevedeva di impostare la \"valuta predefinita\". Questa impostazione \u00e8 ora legata all'amministrazione finanziaria e pu\u00f2 essere diversa per ogni amministrazione.",
+ "administration_currency_form_help": "Se modifichi la valuta principale, il caricamento della pagina potrebbe richiedere molto tempo, poich\u00e9 potrebbe essere necessario convertire la transazione nella (nuova) valuta principale.",
"administrations_page_edit_sub_title_js": "Modifica amministrazione finanziaria \"{title}\"",
"table": "Tabella",
"welcome_back": "La tua situazione finanziaria",
@@ -102,23 +102,23 @@
"profile_oauth_client_secret_title": "Segreto del client",
"profile_oauth_client_secret_expl": "Ecco il segreto del nuovo client. Questa \u00e8 l'unica occasione in cui viene mostrato pertanto non perderlo! Ora puoi usare questo segreto per effettuare delle richieste alle API.",
"profile_oauth_confidential": "Riservato",
- "profile_oauth_confidential_help": "Require the client to authenticate with a secret. Confidential clients can hold credentials in a secure way without exposing them to unauthorized parties. Public applications, such as native desktop or JavaScript SPA applications, are unable to hold secrets securely.",
+ "profile_oauth_confidential_help": "Richiedere al client di autenticarsi con un segreto. I client riservati possono conservare le credenziali in modo sicuro senza esporle a soggetti non autorizzati. Le applicazioni pubbliche, come le applicazioni desktop native o le applicazioni SPA JavaScript, non sono in grado di conservare i segreti in modo sicuro.",
"multi_account_warning_unknown": "A seconda del tipo di transazione che hai creato, il conto di origine e\/o destinazione delle successive suddivisioni pu\u00f2 essere sovrascritto da qualsiasi cosa sia definita nella prima suddivisione della transazione.",
"multi_account_warning_withdrawal": "Ricorda che il conto di origine delle successive suddivisioni verr\u00e0 sovrascritto da quello definito nella prima suddivisione del prelievo.",
"multi_account_warning_deposit": "Ricorda che il conto di destinazione delle successive suddivisioni verr\u00e0 sovrascritto da quello definito nella prima suddivisione del deposito.",
"multi_account_warning_transfer": "Ricorda che il conto di origine e il conto di destinazione delle successive suddivisioni verranno sovrascritti da quelli definiti nella prima suddivisione del trasferimento.",
- "webhook_trigger_ANY": "After any event",
+ "webhook_trigger_ANY": "Dopo ogni evento",
"webhook_trigger_STORE_TRANSACTION": "Dopo aver creato la transazione",
"webhook_trigger_UPDATE_TRANSACTION": "Dopo aver aggiornato la transazione",
"webhook_trigger_DESTROY_TRANSACTION": "Dopo aver eliminato la transazione",
- "webhook_trigger_STORE_BUDGET": "After budget creation",
- "webhook_trigger_UPDATE_BUDGET": "After budget update",
- "webhook_trigger_DESTROY_BUDGET": "After budget delete",
- "webhook_trigger_STORE_UPDATE_BUDGET_LIMIT": "After budgeted amount change",
+ "webhook_trigger_STORE_BUDGET": "Dopo la creazione del budget",
+ "webhook_trigger_UPDATE_BUDGET": "Dopo l'aggiornamento del budget",
+ "webhook_trigger_DESTROY_BUDGET": "Dopo l'eliminazione del budget",
+ "webhook_trigger_STORE_UPDATE_BUDGET_LIMIT": "Dopo la modifica dell'importo preventivato",
"webhook_response_TRANSACTIONS": "Dettagli transazione",
- "webhook_response_RELEVANT": "Relevant details",
+ "webhook_response_RELEVANT": "Dettagli rilevanti",
"webhook_response_ACCOUNTS": "Dettagli conto",
- "webhook_response_NONE": "No details",
+ "webhook_response_NONE": "Nessun dettaglio",
"webhook_delivery_JSON": "JSON",
"actions": "Azioni",
"meta_data": "Meta dati",
@@ -160,7 +160,7 @@
"url": "URL",
"active": "Attivo",
"interest_date": "Data di valuta",
- "administration_currency": "Primary currency",
+ "administration_currency": "Valuta primaria",
"title": "Titolo",
"date": "Data",
"book_date": "Data contabile",
@@ -180,7 +180,7 @@
"list": {
"title": "Titolo",
"active": "\u00c8 attivo?",
- "primary_currency": "Primary currency",
+ "primary_currency": "Valuta primaria",
"trigger": "Trigger",
"response": "Risposta",
"delivery": "Consegna",
diff --git a/resources/assets/v2/src/pages/dashboard/accounts.js b/resources/assets/v2/src/pages/dashboard/accounts.js
index 5aab5eba4d..1361b0008c 100644
--- a/resources/assets/v2/src/pages/dashboard/accounts.js
+++ b/resources/assets/v2/src/pages/dashboard/accounts.js
@@ -211,14 +211,6 @@ export default () => ({
(new Get).show(accountId, new Date(window.store.get('end'))).then((response) => {
let parent = response.data.data;
- // apply function to each element of parent:
- // parent.attributes.balances = parent.attributes.balances.map((balance) => {
- // balance.amount_formatted = formatMoney(balance.amount, balance.currency_code);
- // return balance;
- // });
- // console.log(parent);
-
-
// get groups for account:
const params = {
page: 1,
@@ -261,11 +253,14 @@ export default () => ({
accounts.push({
name: parent.attributes.name,
order: parent.attributes.order,
+
+ current_balance: formatMoney(parent.attributes.current_balance, parent.attributes.currency_code),
+ pc_current_balance: null === parent.attributes.pc_current_balance ? null : formatMoney(parent.attributes.pc_current_balance, parent.attributes.primary_currency_code),
+
id: parent.id,
- balances: parent.attributes.balances,
+ //balances: parent.attributes.balances,
groups: groups,
});
- // console.log(parent.attributes);
count++;
if (count === totalAccounts) {
accounts.sort((a, b) => a.order - b.order); // b - a for reverse sort
diff --git a/resources/assets/v2/src/pages/dashboard/categories.js b/resources/assets/v2/src/pages/dashboard/categories.js
index d75da3acfb..bc2707e805 100644
--- a/resources/assets/v2/src/pages/dashboard/categories.js
+++ b/resources/assets/v2/src/pages/dashboard/categories.js
@@ -54,46 +54,69 @@ export default () => ({
if (data.hasOwnProperty(i)) {
let current = data[i];
let code = current.currency_code;
- if (!series.hasOwnProperty(code)) {
- series[code] = {
- name: code,
- yAxisID: '',
- data: {},
- };
+
+ // create two series, "spent" and "earned".
+ for(const type of ['spent', 'earned']) {
+ let typeCode = code + '_' + type;
+ if (!series.hasOwnProperty(typeCode)) {
+ series[typeCode] = {
+ name: typeCode,
+ code: code,
+ type: type,
+ yAxisID: '',
+ data: {},
+ };
+ }
+ }
+ if (!currencies.includes(code)) {
currencies.push(code);
}
}
}
-
// loop data again to add amounts to each series.
for (const i in data) {
if (data.hasOwnProperty(i)) {
let yAxis = 'y';
let current = data[i];
+
+ // allow switch to primary currency.
let code = current.currency_code;
+ if(this.convertToPrimary) {
+ code = current.primary_currency_code;
+ }
- // loop series, add 0 if not present or add actual amount.
- for (const ii in series) {
- if (series.hasOwnProperty(ii)) {
- let amount = 0.0;
- if (code === ii) {
- // this series' currency matches this column's currency.
- amount = parseFloat(current.amount);
- yAxis = 'y' + current.currency_code;
- }
- if (series[ii].data.hasOwnProperty(current.label)) {
- // there is a value for this particular currency. The amount from this column will be added.
- // (even if this column isn't recorded in this currency and a new filler value is written)
- // this is so currency conversion works.
- series[ii].data[current.label] = series[ii].data[current.label] + amount;
- }
+ // twice again, for speny AND earned.
+ for(const type of ['spent', 'earned']) {
+ let typeCode = code + '_' + type;
+ // loop series, add 0 if not present or add actual amount.
+ for (const ii in series) {
+ if (series.hasOwnProperty(typeCode)) {
+ let amount = 0.0;
+ if (typeCode === ii) {
+ // this series' currency matches this column's currency.
+ amount = parseFloat(current.entries[type]);
+ if(this.convertToPrimary) {
+ amount = parseFloat(current.entries.pc_entries[type]);
+ }
+ yAxis = 'y' + typeCode;
+ }
+ if (series[typeCode].data.hasOwnProperty(current.label)) {
+ // there is a value for this particular currency. The amount from this column will be added.
+ // (even if this column isn't recorded in this currency and a new filler value is written)
+ // this is so currency conversion works.
+ series[typeCode].data[current.label] = series[typeCode].data[current.label] + amount;
+ }
- if (!series[ii].data.hasOwnProperty(current.label)) {
- // this column's amount is not yet set in this series.
- series[ii].data[current.label] = amount;
+ if (!series[typeCode].data.hasOwnProperty(current.label)) {
+ // this column's amount is not yet set in this series.
+ series[typeCode].data[current.label] = amount;
+ }
}
}
}
+
+
+
// add label to x-axis, not unimportant.
if (!options.data.labels.includes(current.label)) {
options.data.labels.push(current.label);
@@ -103,11 +126,11 @@ export default () => ({
// loop the series and create ChartJS-compatible data sets.
let count = 0;
for (const i in series) {
- // console.log('series');
let yAxisID = 'y' + i;
+ let currencyCode = i.replace('_spent', '').replace('_earned', '');
let dataset = {
label: i,
- currency_code: i,
+ currency_code: currencyCode,
yAxisID: yAxisID,
data: [],
// backgroundColor: getColors(null, 'background'),
@@ -148,16 +171,15 @@ export default () => ({
const end = new Date(window.store.get('end'));
const cacheKey = getCacheKey('ds_ct_chart', {convertToPrimary: this.convertToPrimary, start: start, end: end});
- const cacheValid = window.store.get('cacheValid');
+ // const cacheValid = window.store.get('cacheValid');
+ const cacheValid = false;
let cachedData = window.store.get(cacheKey);
-
if (cacheValid && typeof cachedData !== 'undefined') {
chartData = cachedData; // save chart data for later.
this.drawChart(this.generateOptions(chartData));
this.loading = false;
return;
}
-
const dashboard = new Dashboard();
dashboard.dashboard(start, end, null).then((response) => {
chartData = response.data; // save chart data for later.
@@ -181,7 +203,6 @@ export default () => ({
this.getFreshData();
},
init() {
- // console.log('categories init');
Promise.all([getVariable('convert_to_primary', false),]).then((values) => {
this.convertToPrimary = values[0];
afterPromises = true;
diff --git a/resources/assets/v2/src/pages/transactions/edit.js b/resources/assets/v2/src/pages/transactions/edit.js
index e95eca4ca4..cae648d243 100644
--- a/resources/assets/v2/src/pages/transactions/edit.js
+++ b/resources/assets/v2/src/pages/transactions/edit.js
@@ -72,8 +72,6 @@ let transactions = function () {
resetButton: true,
rulesButton: true,
webhooksButton: true,
-
-
},
// form behaviour during transaction
@@ -85,7 +83,7 @@ let transactions = function () {
// form data (except transactions) is stored in formData
formData: {
- defaultCurrency: null,
+ primaryCurrency: null,
enabledCurrencies: [],
primaryCurrencies: [],
foreignCurrencies: [],
@@ -200,8 +198,7 @@ let transactions = function () {
// addedSplit, is called from the HTML
// for source account
const renderAccount = function (item, b, c) {
- console.log(item);
- return item.title + '
' + i18next.t('firefly.account_type_' + item.meta.type) + '';
+ return item.name_with_balance + '
' + i18next.t('firefly.account_type_' + item.type) + '';
};
addAutocomplete({
selector: 'input.ac-source',
@@ -209,7 +206,7 @@ let transactions = function () {
account_types: this.filters.source,
onRenderItem: renderAccount,
valueField: 'id',
- labelField: 'title',
+ labelField: 'name',
onChange: changeSourceAccount,
onSelectItem: selectSourceAccount
});
@@ -217,7 +214,7 @@ let transactions = function () {
selector: 'input.ac-dest',
serverUrl: urls.account,
valueField: 'id',
- labelField: 'title',
+ labelField: 'name',
account_types: this.filters.destination,
onRenderItem: renderAccount,
onChange: changeDestinationAccount,
@@ -227,7 +224,7 @@ let transactions = function () {
selector: 'input.ac-category',
serverUrl: urls.category,
valueField: 'id',
- labelField: 'title',
+ labelField: 'name',
onChange: changeCategory,
onSelectItem: changeCategory
});
@@ -330,7 +327,7 @@ let transactions = function () {
// load meta data.
loadCurrencies().then(data => {
this.formStates.loadingCurrencies = false;
- this.formData.defaultCurrency = data.defaultCurrency;
+ this.formData.primaryCurrency = data.primaryCurrency;
this.formData.enabledCurrencies = data.enabledCurrencies;
this.formData.primaryCurrencies = data.primaryCurrencies;
this.formData.foreignCurrencies = data.foreignCurrencies;
diff --git a/resources/assets/v2/src/pages/transactions/shared/load-currencies.js b/resources/assets/v2/src/pages/transactions/shared/load-currencies.js
index 11c05baf67..2580a1389b 100644
--- a/resources/assets/v2/src/pages/transactions/shared/load-currencies.js
+++ b/resources/assets/v2/src/pages/transactions/shared/load-currencies.js
@@ -28,7 +28,7 @@ export function loadCurrencies() {
let getter = new Get();
return getter.list(params).then((response) => {
let returnData = {
- defaultCurrency: {},
+ primaryCurrency: {},
primaryCurrencies: [],
foreignCurrencies: [],
enabledCurrencies: [],
@@ -46,13 +46,13 @@ export function loadCurrencies() {
id: current.id,
name: current.attributes.name,
code: current.attributes.code,
- default: current.attributes.default,
+ primary: current.attributes.primary,
symbol: current.attributes.symbol,
decimal_places: current.attributes.decimal_places,
};
- if (obj.default) {
- returnData.defaultCurrency = obj;
+ if (obj.primary) {
+ returnData.primaryCurrency = obj;
}
returnData.enabledCurrencies.push(obj);
returnData.primaryCurrencies.push(obj);
diff --git a/resources/lang/en_US/validation.php b/resources/lang/en_US/validation.php
index 9c9052b050..f7b27c0da6 100644
--- a/resources/lang/en_US/validation.php
+++ b/resources/lang/en_US/validation.php
@@ -24,9 +24,10 @@
declare(strict_types=1);
return [
+ 'limit_exists' => 'There is already a budget limit (amount) for this budget and currency in the given period.',
'invalid_sort_instruction' => 'The sort instruction is invalid for an object of type ":object".',
- 'invalid_sort_instruction_index' => 'The sort instruction at index #:index is invalid for an object of type ":object".',
- 'no_sort_instructions' => 'There are no sort instructions defined for an object of type ":object".',
+ 'invalid_sort_instruction_index' => 'The sort instruction at index #:index is invalid for an object of type ":object".',
+ 'no_sort_instructions' => 'There are no sort instructions defined for an object of type ":object".',
'webhook_budget_info' => 'Cannot deliver budget information for transaction related webhooks.',
'webhook_account_info' => 'Cannot deliver account information for budget related webhooks.',
'webhook_transaction_info' => 'Cannot deliver transaction information for budget related webhooks.',
@@ -39,8 +40,8 @@ return [
'nog_logged_in' => 'You are not logged in.',
'prohibited' => 'You must not submit anything in field.',
'bad_webhook_combination' => 'Webhook trigger ":trigger" cannot be combined with webhook response ":response".',
- 'unknown_webhook_trigger' => 'Unknown webhook trigger ":trigger".',
- 'only_any_trigger' => 'If you select the "Any event"-trigger, you may not select any other triggers.',
+ 'unknown_webhook_trigger' => 'Unknown webhook trigger ":trigger".',
+ 'only_any_trigger' => 'If you select the "Any event"-trigger, you may not select any other triggers.',
'bad_type_source' => 'Firefly III can\'t determine the transaction type based on this source account.',
'bad_type_destination' => 'Firefly III can\'t determine the transaction type based on this destination account.',
'missing_where' => 'Array is missing "where"-clause',
@@ -122,7 +123,7 @@ return [
'between.file' => 'The :attribute must be between :min and :max kilobytes.',
'between.string' => 'The :attribute must be between :min and :max characters.',
'between.array' => 'The :attribute must have between :min and :max items.',
- 'between_date' => 'The date must be between the given start and end date.',
+ 'between_date' => 'The date must be between the given start and end date.',
'boolean' => 'The :attribute field must be true or false.',
'confirmed' => 'The :attribute confirmation does not match.',
'date' => 'The :attribute is not a valid date.',
diff --git a/resources/views/list/groups.twig b/resources/views/list/groups.twig
index 460462e684..a53f271fd8 100644
--- a/resources/views/list/groups.twig
+++ b/resources/views/list/groups.twig
@@ -268,7 +268,7 @@
{% if config('firefly.feature_flags.running_balance_column') %}