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') %} - {% if null == transaction.balance_dirty or false == transaction.balance_dirty and null != transaction.destination_balance_after %} + {% if (null == transaction.balance_dirty or false == transaction.balance_dirty) and null != transaction.destination_balance_after and null != transaction.source_balance_after %} {% if transaction.transaction_type_type == 'Deposit' %} {{ formatAmountBySymbol(transaction.destination_balance_after, transaction.currency_symbol, transaction.currency_decimal_places) }} {% elseif transaction.transaction_type_type == 'Withdrawal' %} diff --git a/resources/views/recurring/edit.twig b/resources/views/recurring/edit.twig index 725337de76..37df0d26b6 100644 --- a/resources/views/recurring/edit.twig +++ b/resources/views/recurring/edit.twig @@ -134,9 +134,9 @@ {# BILL ONLY WHEN CREATING A WITHDRAWAL #} {% if bills|length > 1 %} - {{ ExpandedForm.select('bill_id', bills, array.transactions[0].bill_id) }} + {{ ExpandedForm.select('bill_id', bills, array.transactions[0].subscription_id) }} {% else %} - {{ ExpandedForm.select('bill_id', bills, array.transactions[0].bill_id, {helpText: trans('firefly.no_bill_pointer', {link: route('subscriptions.index')})}) }} + {{ ExpandedForm.select('bill_id', bills, array.transactions[0].subscription_id, {helpText: trans('firefly.no_bill_pointer', {link: route('subscriptions.index')})}) }} {% endif %} {# TAGS #} diff --git a/resources/views/transactions/show.twig b/resources/views/transactions/show.twig index 9202da88dd..1b9463fc83 100644 --- a/resources/views/transactions/show.twig +++ b/resources/views/transactions/show.twig @@ -203,7 +203,7 @@ {% set boxSize = 4 %} {% endif %}
- {% for index,journal in selectedGroup.transactions %} + {% for index, journal in selectedGroup.transactions %}
@@ -440,7 +440,7 @@ {{ 'tags'|_ }} {% for tag in journal.tags %} - {% if null != tag.id %} + {% if null != tag.id and '' != tag.id %}

{{ tag.tag }}

{% endif %} {% endfor %} diff --git a/resources/views/v2/partials/dashboard/account-list.blade.php b/resources/views/v2/partials/dashboard/account-list.blade.php index 9934551588..55170d2466 100644 --- a/resources/views/v2/partials/dashboard/account-list.blade.php +++ b/resources/views/v2/partials/dashboard/account-list.blade.php @@ -9,16 +9,10 @@

- - + - + () +