Compare commits

...

5 Commits

Author SHA1 Message Date
github-actions[bot]
29c51ad0e2 Merge pull request #12046 from firefly-iii/release-1774795569
🤖 Automatically merge the PR into the develop branch.
2026-03-29 16:46:17 +02:00
JC5
bf8c40d502 🤖 Auto commit for release 'develop' on 2026-03-29 2026-03-29 16:46:09 +02:00
James Cole
99912483de Add some debug for https://github.com/orgs/firefly-iii/discussions/12044 2026-03-29 16:38:49 +02:00
James Cole
a3f4ab9b1b Validate if amount can be added. 2026-03-27 07:05:16 +01:00
James Cole
953fe7d9eb Fix https://github.com/firefly-iii/firefly-iii/issues/12034 2026-03-27 05:23:22 +01:00
11 changed files with 173 additions and 57 deletions

View File

@@ -116,6 +116,7 @@ final class PiggyBankController extends Controller
'currency_decimal_places' => $currency->decimal_places,
'object_group_id' => null === $objectGroup ? null : (string) $objectGroup->id,
'object_group_title' => $objectGroup?->title,
'object_group_order' => $objectGroup?->order,
];
}

View File

@@ -28,6 +28,7 @@ use FireflyIII\Models\PiggyBank;
use FireflyIII\Rules\IsValidPositiveAmount;
use FireflyIII\Rules\IsValidZeroOrMoreAmount;
use FireflyIII\Rules\LessThanPiggyTarget;
use FireflyIII\Rules\PiggyBank\IsEnoughInAccounts;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Foundation\Http\FormRequest;
@@ -84,7 +85,7 @@ class UpdateRequest extends FormRequest
'accounts' => 'array',
'accounts.*' => 'array',
'accounts.*.account_id' => ['required', 'numeric', 'belongsToUser:accounts,id'],
'accounts.*.current_amount' => ['numeric', 'nullable', new IsValidZeroOrMoreAmount(true)],
'accounts.*.current_amount' => ['numeric', 'nullable', new IsValidZeroOrMoreAmount(true), new IsEnoughInAccounts($piggyBank, $this->getAll())],
'object_group_id' => 'numeric|belongsToUser:object_groups,id',
'object_group_title' => ['min:1', 'max:255'],
'transaction_currency_id' => 'exists:transaction_currencies,id|nullable',

View File

@@ -33,6 +33,7 @@ use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\TransactionJournal;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class CorrectsGroupAccounts extends Command
{
@@ -46,6 +47,7 @@ class CorrectsGroupAccounts extends Command
*/
public function handle(): int
{
Log::debug('Start of correction:group-accounts');
$groups = [];
$res = TransactionJournal::groupBy('transaction_group_id')->get(['transaction_group_id', DB::raw('COUNT(transaction_group_id) as the_count')]);
@@ -64,8 +66,10 @@ class CorrectsGroupAccounts extends Command
$group = TransactionGroup::find($groupId);
$objects->appendFromTransactionGroup($group);
}
Log::debug(sprintf('Fire event for %d transaction group(s)', count($groups)));
event(new UpdatedSingleTransactionGroup($flags, $objects));
event(new WebhookMessagesRequestSending());
Log::debug('End of correction:group-accounts');
return 0;
}

View File

@@ -134,6 +134,13 @@ class PiggyBankFactory
$previous = $toBeLinked[$account->id]['current_amount'] ?? '0';
$diff = bcsub($info['current_amount'], $previous);
// if money is added, check if we can!
if (1 === bccomp($diff, '0') && !$this->piggyBankRepository->canAddAmount($piggyBank, $account, $diff)) {
Log::debug(sprintf('Cannot add amount %s to piggy bank #%d ("%s")', $diff, $piggyBank->id, $piggyBank->name));
continue;
}
// create event for difference.
if (0 !== bccomp($diff, '0')) {
// 2025-10-01 for issue #10990 disable this event.

View File

@@ -116,13 +116,28 @@ class ProcessesUpdatedTransactionGroup
$destAccount = $first->transactions()->where('amount', '>', '0')->first()->account;
$type = $first->transactionType->type;
$effect = 0;
if (TransactionTypeEnum::TRANSFER->value === $type || TransactionTypeEnum::WITHDRAWAL->value === $type) {
// set all source transactions to source account:
Transaction::whereIn('transaction_journal_id', $all)->where('amount', '<', 0)->update(['account_id' => $sourceAccount->id]);
$effect += Transaction::whereIn('transaction_journal_id', $all)
->where('account_id', '!=', $sourceAccount->id)
->where('amount', '<', 0)
->update(['account_id' => $sourceAccount->id])
;
}
if (TransactionTypeEnum::TRANSFER->value === $type || TransactionTypeEnum::DEPOSIT->value === $type) {
// set all destination transactions to destination account:
Transaction::whereIn('transaction_journal_id', $all)->where('amount', '>', 0)->update(['account_id' => $destAccount->id]);
$effect += Transaction::whereIn('transaction_journal_id', $all)
->where('account_id', '!=', $destAccount->id)
->where('amount', '>', 0)
->update(['account_id' => $destAccount->id])
;
}
if (0 === $effect) {
Log::debug(sprintf('Had nothing to do in unifyAccounts(#%d)', $group->id));
return;
}
Log::debug(sprintf('Updated %d transaction(s) in unifyAccounts(#%d)', $effect, $group->id));
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/*
* IsEnoughInAccounts.php
* Copyright (c) 2026 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace FireflyIII\Rules\PiggyBank;
use Closure;
use FireflyIII\Models\PiggyBank;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use Illuminate\Contracts\Validation\ValidationRule;
use Override;
class IsEnoughInAccounts implements ValidationRule
{
public function __construct(
private readonly PiggyBank $piggyBank,
private readonly array $data
) {}
#[Override]
public function validate(string $attribute, mixed $value, Closure $fail): void
{
// TODO: Implement validate() method.
if (!array_key_exists('accounts', $this->data)) {
return;
}
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
/** @var PiggyBankRepositoryInterface $piggyRepos */
$piggyRepos = app(PiggyBankRepositoryInterface::class);
$accounts = $this->data['accounts'];
foreach ($accounts as $info) {
$account = $repository->find((int) $info['account_id']);
$amount = $info['current_amount'] ?? '0';
if (null === $account) {
$fail('validation.no_asset_account')->translate();
return;
}
if ('' === $amount || 0 === bccomp($amount, '0')) {
$fail('validation.more_than_zero_correct')->translate();
return;
}
$diff = bcsub($amount, $piggyRepos->getCurrentAmount($this->piggyBank, $account));
if (1 === bccomp($diff, '0') && !$piggyRepos->canAddAmount($this->piggyBank, $account, $amount)) {
$fail('validation.cannot_add_piggy_amount')->translate();
}
}
}
}

View File

@@ -474,7 +474,10 @@ trait ConvertsDataTypes
if (!array_key_exists('current_amount', $entry)) {
$amount = null;
}
$return[] = ['account_id' => $this->integerFromValue((string) ($entry['account_id'] ?? '0')), 'current_amount' => $amount];
$return[] = [
'account_id' => $this->integerFromValue((string) ($entry['account_id'] ?? '0')),
'current_amount' => $amount,
];
}
return $return;

53
composer.lock generated
View File

@@ -1009,16 +1009,16 @@
},
{
"name": "firebase/php-jwt",
"version": "v7.0.3",
"version": "v7.0.4",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
"reference": "28aa0694bcfdfa5e2959c394d5a1ee7a5083629e"
"reference": "e41f1bd7dbe3c5455c3f72d4338cfeb083b71931"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/28aa0694bcfdfa5e2959c394d5a1ee7a5083629e",
"reference": "28aa0694bcfdfa5e2959c394d5a1ee7a5083629e",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/e41f1bd7dbe3c5455c3f72d4338cfeb083b71931",
"reference": "e41f1bd7dbe3c5455c3f72d4338cfeb083b71931",
"shasum": ""
},
"require": {
@@ -1026,6 +1026,7 @@
},
"require-dev": {
"guzzlehttp/guzzle": "^7.4",
"phpfastcache/phpfastcache": "^9.2",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psr/cache": "^2.0||^3.0",
@@ -1066,9 +1067,9 @@
],
"support": {
"issues": "https://github.com/firebase/php-jwt/issues",
"source": "https://github.com/firebase/php-jwt/tree/v7.0.3"
"source": "https://github.com/firebase/php-jwt/tree/v7.0.4"
},
"time": "2026-02-25T22:16:40+00:00"
"time": "2026-03-27T21:17:19+00:00"
},
{
"name": "fruitcake/php-cors",
@@ -10173,22 +10174,22 @@
},
{
"name": "cloudcreativity/json-api-testing",
"version": "v6.2.0",
"version": "v6.3.0",
"source": {
"type": "git",
"url": "https://github.com/cloudcreativity/json-api-testing.git",
"reference": "bb2ff0a87013d40781ca7f959023491d6f1b4ed2"
"reference": "c6f1460dae648a28217b182195f8ca4ed0ee9bd5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cloudcreativity/json-api-testing/zipball/bb2ff0a87013d40781ca7f959023491d6f1b4ed2",
"reference": "bb2ff0a87013d40781ca7f959023491d6f1b4ed2",
"url": "https://api.github.com/repos/cloudcreativity/json-api-testing/zipball/c6f1460dae648a28217b182195f8ca4ed0ee9bd5",
"reference": "c6f1460dae648a28217b182195f8ca4ed0ee9bd5",
"shasum": ""
},
"require": {
"ext-json": "*",
"illuminate/contracts": "^10.0|^11.0|^12.0",
"illuminate/support": "^10.0|^11.0|^12.0",
"illuminate/contracts": "^10.0|^11.0|^12.0|^13.0",
"illuminate/support": "^10.0|^11.0|^12.0|^13.0",
"php": "^8.2",
"phpunit/phpunit": "^10.5|^11.0|^12.0"
},
@@ -10225,9 +10226,9 @@
],
"support": {
"issues": "https://github.com/cloudcreativity/json-api/issues",
"source": "https://github.com/cloudcreativity/json-api-testing/tree/v6.2.0"
"source": "https://github.com/cloudcreativity/json-api-testing/tree/v6.3.0"
},
"time": "2025-04-09T19:15:46+00:00"
"time": "2026-03-28T18:04:28+00:00"
},
{
"name": "composer/class-map-generator",
@@ -10835,28 +10836,28 @@
},
{
"name": "laravel-json-api/testing",
"version": "v3.1.0",
"version": "v3.2.0",
"source": {
"type": "git",
"url": "https://github.com/laravel-json-api/testing.git",
"reference": "bbf3b31b977955eff93c47ff101c4a134a3ffa8f"
"reference": "506d434a47ebd9bcc7dfca0096c63b79d28427a8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel-json-api/testing/zipball/bbf3b31b977955eff93c47ff101c4a134a3ffa8f",
"reference": "bbf3b31b977955eff93c47ff101c4a134a3ffa8f",
"url": "https://api.github.com/repos/laravel-json-api/testing/zipball/506d434a47ebd9bcc7dfca0096c63b79d28427a8",
"reference": "506d434a47ebd9bcc7dfca0096c63b79d28427a8",
"shasum": ""
},
"require": {
"cloudcreativity/json-api-testing": "^6.1",
"cloudcreativity/json-api-testing": "^6.3",
"ext-json": "*",
"illuminate/http": "^11.0|^12.0",
"illuminate/support": "^11.0|^12.0",
"illuminate/testing": "^11.0|^12.0",
"illuminate/http": "^11.0|^12.0|^13.0",
"illuminate/support": "^11.0|^12.0|^13.0",
"illuminate/testing": "^11.0|^12.0|^13.0",
"php": "^8.2"
},
"require-dev": {
"laravel/framework": "^11.0|^12.0",
"laravel/framework": "^11.0|^12.0|^13.0",
"phpunit/phpunit": "^10.5|^11.0"
},
"type": "library",
@@ -10881,7 +10882,7 @@
},
{
"name": "Christopher Gammie",
"email": "contact@gammie.co.uk"
"email": "chris@cloudcreativity.co.uk"
}
],
"description": "Test helpers for JSON:API compliant APIs.",
@@ -10894,9 +10895,9 @@
],
"support": {
"issues": "https://github.com/laravel-json-api/testing/issues",
"source": "https://github.com/laravel-json-api/testing/tree/v3.1.0"
"source": "https://github.com/laravel-json-api/testing/tree/v3.2.0"
},
"time": "2025-02-24T20:39:08+00:00"
"time": "2026-03-28T18:06:47+00:00"
},
{
"name": "mockery/mockery",

View File

@@ -78,8 +78,8 @@ return [
'running_balance_column' => (bool)envDefaultWhenEmpty(env('USE_RUNNING_BALANCE'), true), // this is only the default value, is not used.
// see cer.php for exchange rates feature flag.
],
'version' => 'develop/2026-03-27',
'build_time' => 1774582537,
'version' => 'develop/2026-03-29',
'build_time' => 1774795378,
'api_version' => '2.1.0', // field is no longer used.
'db_version' => 28, // field is no longer used.

58
package-lock.json generated
View File

@@ -1709,6 +1709,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.0",
"tslib": "^2.4.0"
@@ -1721,6 +1722,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -1732,6 +1734,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -1840,20 +1843,22 @@
"license": "MIT"
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz",
"integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@tybys/wasm-util": "^0.10.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"peerDependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1"
}
},
"node_modules/@nodelib/fs.scandir": {
@@ -3380,9 +3385,9 @@
}
},
"node_modules/alpinejs": {
"version": "3.15.8",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.8.tgz",
"integrity": "sha512-zxIfCRTBGvF1CCLIOMQOxAyBuqibxSEwS6Jm1a3HGA9rgrJVcjEWlwLcQTVGAWGS8YhAsTRLVrtQ5a5QT9bSSQ==",
"version": "3.15.9",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.9.tgz",
"integrity": "sha512-O30m8Tw/aARbLXmeTnISAFgrNm0K71PT7bZy/1NgRqFD36QGb34VJ4a6WBL1iIO/bofN+LkIkKLikUTkfPL2wQ==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "~3.1.1"
@@ -3565,15 +3570,15 @@
}
},
"node_modules/axios": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz",
"integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
"proxy-from-env": "^2.1.0"
}
},
"node_modules/babel-loader": {
@@ -3677,9 +3682,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.11",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.11.tgz",
"integrity": "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg==",
"version": "2.10.12",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.12.tgz",
"integrity": "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -3848,9 +3853,9 @@
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5333,9 +5338,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.327",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.327.tgz",
"integrity": "sha512-hLxLdIJDf8zIzKoH2TPCs+Botc+wUmj9sp4jVMwklY/sKleM8xxxOExRX3Gxj73nCXmJe3anhG7SvsDDPDvmuQ==",
"version": "1.5.328",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.328.tgz",
"integrity": "sha512-QNQ5l45DzYytThO21403XN3FvK0hOkWDG8viNf6jqS42msJ8I4tGDSpBCgvDRRPnkffafiwAym2X2eHeGD2V0w==",
"dev": true,
"license": "ISC"
},
@@ -9460,11 +9465,14 @@
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"dev": true,
"license": "MIT"
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/pseudomap": {
"version": "1.0.2",

View File

@@ -70,6 +70,7 @@ return [
'rule_action_value' => 'This value is invalid for the selected action.',
'file_already_attached' => 'Uploaded file ":name" is already attached to this object.',
'file_attached' => 'Successfully uploaded file ":name".',
'cannot_add_piggy_amount' => 'This amount cannot be added to the piggy bank.',
'file_zero' => 'The file is zero bytes in size.',
'must_exist' => 'The ID in field :attribute does not exist in the database.',
'all_accounts_equal' => 'All accounts in this field must be equal.',