mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2026-03-22 21:24:20 +00:00
Compare commits
57 Commits
develop-20
...
develop-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc60025097 | ||
|
|
d77769b2f4 | ||
|
|
c6497960f8 | ||
|
|
4eee0c79cd | ||
|
|
e333c3254b | ||
|
|
9d244883a1 | ||
|
|
a17131c2f7 | ||
|
|
898459198d | ||
|
|
6466bc9272 | ||
|
|
dd8a8dba85 | ||
|
|
ee16888317 | ||
|
|
56a2580fd7 | ||
|
|
2ab0225223 | ||
|
|
83662415c3 | ||
|
|
cf976b2ab1 | ||
|
|
bf79c9db72 | ||
|
|
29f4c09a7b | ||
|
|
22ef456dca | ||
|
|
9c706465b2 | ||
|
|
a40425fd75 | ||
|
|
e9a37592ba | ||
|
|
0c598cb034 | ||
|
|
f5dea9ac09 | ||
|
|
4ef7944147 | ||
|
|
4e1c84944c | ||
|
|
f36da26cc3 | ||
|
|
5983a8eb6d | ||
|
|
b4a8a219ff | ||
|
|
4190c4d243 | ||
|
|
70cbbc1523 | ||
|
|
c724f13501 | ||
|
|
5f01a83b43 | ||
|
|
53c13d221d | ||
|
|
266cd7d8d0 | ||
|
|
7c09278c8e | ||
|
|
21af34c65a | ||
|
|
594c04b121 | ||
|
|
c50408249b | ||
|
|
b05a38c0e2 | ||
|
|
0bb1afdf6c | ||
|
|
547b83b36e | ||
|
|
134d8c8cf6 | ||
|
|
94144a407d | ||
|
|
15e29d133a | ||
|
|
21f9be6504 | ||
|
|
d514792f4d | ||
|
|
5894695ad6 | ||
|
|
7004c9aaf5 | ||
|
|
1893a33d84 | ||
|
|
d345b31cd4 | ||
|
|
a27642024d | ||
|
|
c23ad831d0 | ||
|
|
d50c283973 | ||
|
|
9ea3519585 | ||
|
|
ddb5bc6038 | ||
|
|
caadef7c64 | ||
|
|
974c84a877 |
55
.github/pull_request_template.md
vendored
55
.github/pull_request_template.md
vendored
@@ -1,25 +1,50 @@
|
||||
<!--
|
||||
|
||||
Please TALK TO ME FIRST before you open a PR.
|
||||
🙌 Thanks for contributing a pull request. Before you continue:
|
||||
|
||||
1. If you fix a problem that has no ticket, talk to me FIRST.
|
||||
2. If you introduce new financial solutions or concepts, talk to me FIRST.
|
||||
3. If your PR is more than 25 lines, talk to me FIRST.
|
||||
4. If you used AI to write your PR, talk to me FIRST.
|
||||
5. If you fix spelling or code comments, talk to me FIRST.
|
||||
1. If you introduce new financial solutions or concepts, talk to me FIRST.
|
||||
2. If your PR is more than 25 lines, talk to me FIRST.
|
||||
3. If you fix spelling or code comments, talk to me FIRST.
|
||||
|
||||
Wanna talk to me? Open a GitHub Issue, Discussion, or send me an email: james@firefly-iii.org
|
||||
Wanna talk to me? Open a GitHub Issue, Discussion, or email me: james@firefly-iii.org
|
||||
|
||||
See also: https://docs.firefly-iii.org/explanation/support/#contributing-code
|
||||
👀 Please ensure you have taken a look at the contribution guidelines:
|
||||
https://docs.firefly-iii.org/explanation/support/#contributing-code
|
||||
|
||||
Remember that your PR may be CLOSED:
|
||||
|
||||
1. If you do not refer to an existing issue, your PR will be CLOSED.
|
||||
2. If you open a PR on the main branch, your PR will be CLOSED.
|
||||
3. If you only fix a spelling error or code comment, your PR will be CLOSED.
|
||||
|
||||
Thanks again, and happy developing!
|
||||
|
||||
-->
|
||||
|
||||
@JC5
|
||||
|
||||
This PR fixes issue # <!-- mandatory field! -->.
|
||||
#### Reference issues and PRs
|
||||
<!--
|
||||
Example: Fixes #1234. See also #3456.
|
||||
-->
|
||||
|
||||
Changes in this pull request:
|
||||
#### What does this implement/fix? Explain your changes.
|
||||
|
||||
-
|
||||
-
|
||||
-
|
||||
|
||||
|
||||
#### AI usage disclosure
|
||||
<!--
|
||||
If AI tools were involved in creating this PR, please check all boxes that apply
|
||||
below and make sure that you adhere to our Automated Contributions Policy:
|
||||
https://docs.firefly-iii.org/explanation/support/#automated-contributions-policy
|
||||
-->
|
||||
I used AI assistance for:
|
||||
- [ ] Code generation (e.g., when writing an implementation or fixing a bug)
|
||||
- [ ] Test/benchmark generation
|
||||
- [ ] Documentation (including examples)
|
||||
- [ ] Research and understanding
|
||||
|
||||
|
||||
#### Any other comments?
|
||||
|
||||
<!--
|
||||
Thanks for contributing!
|
||||
-->
|
||||
|
||||
2
.github/security.md
vendored
2
.github/security.md
vendored
@@ -106,6 +106,8 @@ found with the full or partial support of AI coding agents, large language model
|
||||
2. explain how the vulnerability can actually be abused by a nefarious third party, and
|
||||
3. try to limit the verbosity of your report.
|
||||
|
||||
At the discretion of the maintainer of the developer, your report may be closed without resolve.
|
||||
|
||||
## Credits
|
||||
|
||||
This security policy is based on [Harbor](https://github.com/goharbor/harbor)'s security policy.
|
||||
|
||||
@@ -4,6 +4,7 @@ Over time, many people have contributed to Firefly III. Their efforts are not al
|
||||
Please find below all the people who contributed to the Firefly III code. Their names are mentioned in the year of their first contribution.
|
||||
|
||||
## 2026
|
||||
- Joe Longendyke
|
||||
- Daniel Holøien
|
||||
- Matthew Grove
|
||||
- Cinnamon Pyro
|
||||
|
||||
@@ -28,6 +28,7 @@ use Carbon\Carbon;
|
||||
use FireflyIII\Api\V1\Controllers\Controller;
|
||||
use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\DestroyRequest;
|
||||
use FireflyIII\Enums\UserRoleEnum;
|
||||
use FireflyIII\Events\Model\CurrencyExchangeRate\DestroyedCurrencyExchangeRate;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Models\CurrencyExchangeRate;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
@@ -59,11 +60,12 @@ final class DestroyController extends Controller
|
||||
public function destroy(DestroyRequest $request, TransactionCurrency $from, TransactionCurrency $to): JsonResponse
|
||||
{
|
||||
$this->repository->deleteRates($from, $to);
|
||||
event(new DestroyedCurrencyExchangeRate($from, $to, $this->validateUserGroup($request)));
|
||||
|
||||
return response()->json([], 204);
|
||||
}
|
||||
|
||||
public function destroySingleByDate(TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
|
||||
public function destroySingleByDate(Request $request, TransactionCurrency $from, TransactionCurrency $to, Carbon $date): JsonResponse
|
||||
{
|
||||
$exchangeRate = $this->repository->getSpecificRateOnDate($from, $to, $date);
|
||||
if ($exchangeRate instanceof CurrencyExchangeRate) {
|
||||
@@ -72,14 +74,19 @@ final class DestroyController extends Controller
|
||||
if (!$exchangeRate instanceof CurrencyExchangeRate) {
|
||||
throw new FireflyException('Bla');
|
||||
}
|
||||
event(new DestroyedCurrencyExchangeRate($from, $to, $this->validateUserGroup($request)));
|
||||
|
||||
return response()->json([], 204);
|
||||
}
|
||||
|
||||
public function destroySingleById(CurrencyExchangeRate $exchangeRate): JsonResponse
|
||||
public function destroySingleById(Request $request, CurrencyExchangeRate $exchangeRate): JsonResponse
|
||||
{
|
||||
$from = $exchangeRate->fromCurrency;
|
||||
$to = $exchangeRate->toCurrency;
|
||||
$this->repository->deleteRate($exchangeRate);
|
||||
|
||||
event(new DestroyedCurrencyExchangeRate($from, $to, $this->validateUserGroup($request)));
|
||||
|
||||
return response()->json([], 204);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreByCurrenciesRequ
|
||||
use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreByDateRequest;
|
||||
use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\StoreRequest;
|
||||
use FireflyIII\Enums\UserRoleEnum;
|
||||
use FireflyIII\Events\Model\CurrencyExchangeRate\CreatedCurrencyExchangeRate;
|
||||
use FireflyIII\Events\Model\CurrencyExchangeRate\UpdatedCurrencyExchangeRate;
|
||||
use FireflyIII\Models\CurrencyExchangeRate;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface;
|
||||
@@ -73,10 +75,12 @@ final class StoreController extends Controller
|
||||
if ($object instanceof CurrencyExchangeRate) {
|
||||
// just update it, no matter.
|
||||
$rate = $this->repository->updateExchangeRate($object, $rate, $date);
|
||||
event(new UpdatedCurrencyExchangeRate($rate));
|
||||
}
|
||||
if (!$object instanceof CurrencyExchangeRate) {
|
||||
// store new
|
||||
$rate = $this->repository->storeExchangeRate($from, $to, $rate, $date);
|
||||
event(new CreatedCurrencyExchangeRate($rate));
|
||||
}
|
||||
|
||||
$transformer = new ExchangeRateTransformer();
|
||||
@@ -97,10 +101,12 @@ final class StoreController extends Controller
|
||||
// update existing rate.
|
||||
$existing = $this->repository->updateExchangeRate($existing, $rate);
|
||||
$collection->push($existing);
|
||||
event(new UpdatedCurrencyExchangeRate($existing));
|
||||
|
||||
continue;
|
||||
}
|
||||
$new = $this->repository->storeExchangeRate($from, $to, $rate, $date);
|
||||
event(new CreatedCurrencyExchangeRate($new));
|
||||
$collection->push($new);
|
||||
}
|
||||
|
||||
@@ -124,11 +130,13 @@ final class StoreController extends Controller
|
||||
// update existing rate.
|
||||
$existing = $this->repository->updateExchangeRate($existing, $rate);
|
||||
$collection->push($existing);
|
||||
event(new UpdatedCurrencyExchangeRate($existing));
|
||||
|
||||
continue;
|
||||
}
|
||||
$new = $this->repository->storeExchangeRate($from, $to, $rate, $date);
|
||||
$collection->push($new);
|
||||
event(new CreatedCurrencyExchangeRate($new));
|
||||
}
|
||||
|
||||
$count = $collection->count();
|
||||
|
||||
@@ -28,6 +28,7 @@ use Carbon\Carbon;
|
||||
use FireflyIII\Api\V1\Controllers\Controller;
|
||||
use FireflyIII\Api\V1\Requests\Models\CurrencyExchangeRate\UpdateRequest;
|
||||
use FireflyIII\Enums\UserRoleEnum;
|
||||
use FireflyIII\Events\Model\CurrencyExchangeRate\UpdatedCurrencyExchangeRate;
|
||||
use FireflyIII\Models\CurrencyExchangeRate;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Repositories\ExchangeRate\ExchangeRateRepositoryInterface;
|
||||
@@ -66,7 +67,7 @@ final class UpdateController extends Controller
|
||||
$date = $request->getDate();
|
||||
$rate = $request->getRate();
|
||||
$exchangeRate = $this->repository->updateExchangeRate($exchangeRate, $rate, $date);
|
||||
|
||||
event(new UpdatedCurrencyExchangeRate($exchangeRate));
|
||||
$transformer = new ExchangeRateTransformer();
|
||||
|
||||
return response()->api($this->jsonApiObject(self::RESOURCE_KEY, $exchangeRate, $transformer))->header('Content-Type', self::CONTENT_TYPE);
|
||||
@@ -77,6 +78,7 @@ final class UpdateController extends Controller
|
||||
$date = $request->getDate();
|
||||
$rate = $request->getRate();
|
||||
$exchangeRate = $this->repository->updateExchangeRate($exchangeRate, $rate, $date);
|
||||
event(new UpdatedCurrencyExchangeRate($exchangeRate));
|
||||
$transformer = new ExchangeRateTransformer();
|
||||
$transformer->setParameters($this->parameters);
|
||||
|
||||
|
||||
@@ -28,9 +28,7 @@ use FireflyIII\Api\V1\Controllers\Controller;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
|
||||
use FireflyIII\Repositories\User\UserRepositoryInterface;
|
||||
use FireflyIII\Support\Facades\Preferences;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
@@ -41,7 +39,6 @@ use Illuminate\Validation\ValidationException;
|
||||
final class DestroyController extends Controller
|
||||
{
|
||||
private CurrencyRepositoryInterface $repository;
|
||||
private UserRepositoryInterface $userRepository;
|
||||
|
||||
/**
|
||||
* CurrencyRepository constructor.
|
||||
@@ -50,8 +47,7 @@ final class DestroyController extends Controller
|
||||
{
|
||||
parent::__construct();
|
||||
$this->middleware(function ($request, $next) {
|
||||
$this->repository = app(CurrencyRepositoryInterface::class);
|
||||
$this->userRepository = app(UserRepositoryInterface::class);
|
||||
$this->repository = app(CurrencyRepositoryInterface::class);
|
||||
$this->repository->setUser(auth()->user());
|
||||
|
||||
return $next($request);
|
||||
@@ -69,15 +65,8 @@ final class DestroyController extends Controller
|
||||
*/
|
||||
public function destroy(TransactionCurrency $currency): JsonResponse
|
||||
{
|
||||
/** @var User $admin */
|
||||
$admin = auth()->user();
|
||||
$rules = ['currency_code' => 'required'];
|
||||
|
||||
if (!$this->userRepository->hasRole($admin, 'owner')) {
|
||||
// access denied:
|
||||
$messages = ['currency_code' => '200005: You need the "owner" role to do this.'];
|
||||
Validator::make([], $rules, $messages)->validate();
|
||||
}
|
||||
if ($this->repository->currencyInUse($currency)) {
|
||||
$messages = ['currency_code' => '200006: Currency in use.'];
|
||||
Validator::make([], $rules, $messages)->validate();
|
||||
|
||||
@@ -35,7 +35,6 @@ use FireflyIII\Support\Http\Api\TransactionFilter;
|
||||
use FireflyIII\Transformers\CurrencyTransformer;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use League\Fractal\Resource\Item;
|
||||
|
||||
/**
|
||||
@@ -154,7 +153,6 @@ final class UpdateController extends Controller
|
||||
public function update(UpdateRequest $request, TransactionCurrency $currency): JsonResponse
|
||||
{
|
||||
$data = $request->getAll();
|
||||
Log::debug(__METHOD__, $data);
|
||||
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
@@ -32,7 +32,6 @@ use FireflyIII\Support\Facades\Preferences;
|
||||
use FireflyIII\Support\Http\Api\TransactionFilter;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class DestroyController
|
||||
@@ -72,11 +71,6 @@ final class DestroyController extends Controller
|
||||
if (false === $linkType->editable) {
|
||||
throw new FireflyException('200020: Link type cannot be changed.');
|
||||
}
|
||||
if (false === auth()->user()->hasRole('owner')) {
|
||||
Log::channel('audit')->warning('Non-owner user tries to delete a link type.');
|
||||
|
||||
response()->json([], 401);
|
||||
}
|
||||
|
||||
$this->repository->destroy($linkType);
|
||||
Preferences::mark();
|
||||
|
||||
@@ -27,12 +27,10 @@ namespace FireflyIII\Api\V1\Controllers\Models\TransactionLinkType;
|
||||
use FireflyIII\Api\V1\Controllers\Controller;
|
||||
use FireflyIII\Api\V1\Requests\Models\TransactionLinkType\StoreRequest;
|
||||
use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface;
|
||||
use FireflyIII\Repositories\User\UserRepositoryInterface;
|
||||
use FireflyIII\Support\Http\Api\TransactionFilter;
|
||||
use FireflyIII\Transformers\LinkTypeTransformer;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use League\Fractal\Resource\Item;
|
||||
|
||||
@@ -44,7 +42,6 @@ final class StoreController extends Controller
|
||||
use TransactionFilter;
|
||||
|
||||
private LinkTypeRepositoryInterface $repository;
|
||||
private UserRepositoryInterface $userRepository;
|
||||
|
||||
/**
|
||||
* LinkTypeController constructor.
|
||||
@@ -54,9 +51,8 @@ final class StoreController extends Controller
|
||||
parent::__construct();
|
||||
$this->middleware(function ($request, $next) {
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
$this->repository = app(LinkTypeRepositoryInterface::class);
|
||||
$this->userRepository = app(UserRepositoryInterface::class);
|
||||
$user = auth()->user();
|
||||
$this->repository = app(LinkTypeRepositoryInterface::class);
|
||||
$this->repository->setUser($user);
|
||||
|
||||
return $next($request);
|
||||
@@ -73,15 +69,6 @@ final class StoreController extends Controller
|
||||
*/
|
||||
public function store(StoreRequest $request): JsonResponse
|
||||
{
|
||||
/** @var User $admin */
|
||||
$admin = auth()->user();
|
||||
$rules = ['name' => 'required'];
|
||||
|
||||
if (!$this->userRepository->hasRole($admin, 'owner')) {
|
||||
// access denied:
|
||||
$messages = ['name' => '200005: You need the "owner" role to do this.'];
|
||||
Validator::make([], $rules, $messages)->validate();
|
||||
}
|
||||
$data = $request->getAll();
|
||||
// if currency ID is 0, find the currency by the code:
|
||||
$linkType = $this->repository->store($data);
|
||||
|
||||
@@ -29,12 +29,10 @@ use FireflyIII\Api\V1\Requests\Models\TransactionLinkType\UpdateRequest;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Models\LinkType;
|
||||
use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface;
|
||||
use FireflyIII\Repositories\User\UserRepositoryInterface;
|
||||
use FireflyIII\Support\Http\Api\TransactionFilter;
|
||||
use FireflyIII\Transformers\LinkTypeTransformer;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use League\Fractal\Resource\Item;
|
||||
|
||||
@@ -46,7 +44,6 @@ final class UpdateController extends Controller
|
||||
use TransactionFilter;
|
||||
|
||||
private LinkTypeRepositoryInterface $repository;
|
||||
private UserRepositoryInterface $userRepository;
|
||||
|
||||
/**
|
||||
* LinkTypeController constructor.
|
||||
@@ -56,9 +53,8 @@ final class UpdateController extends Controller
|
||||
parent::__construct();
|
||||
$this->middleware(function ($request, $next) {
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
$this->repository = app(LinkTypeRepositoryInterface::class);
|
||||
$this->userRepository = app(UserRepositoryInterface::class);
|
||||
$user = auth()->user();
|
||||
$this->repository = app(LinkTypeRepositoryInterface::class);
|
||||
$this->repository->setUser($user);
|
||||
|
||||
return $next($request);
|
||||
@@ -80,15 +76,6 @@ final class UpdateController extends Controller
|
||||
throw new FireflyException('200020: Link type cannot be changed.');
|
||||
}
|
||||
|
||||
/** @var User $admin */
|
||||
$admin = auth()->user();
|
||||
$rules = ['name' => 'required'];
|
||||
|
||||
if (!$this->userRepository->hasRole($admin, 'owner')) {
|
||||
$messages = ['name' => '200005: You need the "owner" role to do this.'];
|
||||
Validator::make([], $rules, $messages)->validate();
|
||||
}
|
||||
|
||||
$data = $request->getAll();
|
||||
$this->repository->update($linkType, $data);
|
||||
$manager = $this->getManager();
|
||||
|
||||
@@ -30,12 +30,10 @@ use FireflyIII\Enums\WebhookDelivery;
|
||||
use FireflyIII\Enums\WebhookResponse;
|
||||
use FireflyIII\Enums\WebhookTrigger;
|
||||
use FireflyIII\Exceptions\FireflyException;
|
||||
use FireflyIII\Repositories\User\UserRepositoryInterface;
|
||||
use FireflyIII\Support\Binder\EitherConfigKey;
|
||||
use FireflyIII\Support\Facades\FireflyConfig;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/**
|
||||
@@ -43,21 +41,6 @@ use Illuminate\Validation\ValidationException;
|
||||
*/
|
||||
final class ConfigurationController extends Controller
|
||||
{
|
||||
private UserRepositoryInterface $repository;
|
||||
|
||||
/**
|
||||
* ConfigurationController constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->middleware(function ($request, $next) {
|
||||
$this->repository = app(UserRepositoryInterface::class);
|
||||
|
||||
return $next($request);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This endpoint is documented at:
|
||||
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/configuration/getConfiguration
|
||||
@@ -142,11 +125,6 @@ final class ConfigurationController extends Controller
|
||||
*/
|
||||
public function update(UpdateRequest $request, string $name): JsonResponse
|
||||
{
|
||||
$rules = ['value' => 'required'];
|
||||
if (!$this->repository->hasRole(auth()->user(), 'owner')) {
|
||||
$messages = ['value' => '200005: You need the "owner" role to do this.'];
|
||||
Validator::make([], $rules, $messages)->validate();
|
||||
}
|
||||
$data = $request->getAll();
|
||||
$shortName = str_replace('configuration.', '', $name);
|
||||
|
||||
|
||||
@@ -74,13 +74,9 @@ final class UserController extends Controller
|
||||
return response()->json([], 500);
|
||||
}
|
||||
|
||||
if ($this->repository->hasRole($admin, 'owner')) {
|
||||
$this->repository->destroy($user);
|
||||
$this->repository->destroy($user);
|
||||
|
||||
return response()->json([], 204);
|
||||
}
|
||||
|
||||
throw new FireflyException('200025: No access to function.');
|
||||
return response()->json([], 204);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,6 +28,7 @@ use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Rules\IsBoolean;
|
||||
use FireflyIII\Support\Request\ChecksLogin;
|
||||
use FireflyIII\Support\Request\ConvertsDataTypes;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
/**
|
||||
@@ -45,15 +46,23 @@ class UpdateRequest extends FormRequest
|
||||
*/
|
||||
public function getAll(): array
|
||||
{
|
||||
// return nothing that isn't explicitly in the array:
|
||||
$fields = [
|
||||
'name' => ['name', 'convertString'],
|
||||
'code' => ['code', 'convertString'],
|
||||
'symbol' => ['symbol', 'convertString'],
|
||||
'decimal_places' => ['decimal_places', 'convertInteger'],
|
||||
'default' => ['default', 'boolean'],
|
||||
'enabled' => ['enabled', 'boolean'],
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
$isAdmin = $user->hasRole('owner');
|
||||
|
||||
$fields = [
|
||||
'enabled' => ['enabled', 'boolean'],
|
||||
];
|
||||
if ($isAdmin) {
|
||||
$fields = [
|
||||
'name' => ['name', 'convertString'],
|
||||
'code' => ['code', 'convertString'],
|
||||
'symbol' => ['symbol', 'convertString'],
|
||||
'decimal_places' => ['decimal_places', 'convertInteger'],
|
||||
'default' => ['default', 'boolean'],
|
||||
'enabled' => ['enabled', 'boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->getAllData($fields);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* CreatedCurrencyExchangeRate.php
|
||||
* Copyright (c) 2026 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Events\Model\CurrencyExchangeRate;
|
||||
|
||||
use FireflyIII\Events\Event;
|
||||
use FireflyIII\Models\CurrencyExchangeRate;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CreatedCurrencyExchangeRate extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public CurrencyExchangeRate $rate
|
||||
) {
|
||||
Log::debug(sprintf('CreatedCurrencyExchangeRate(#%d) Event', $rate->id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* DestroyedCurrencyExchangeRate.php
|
||||
* Copyright (c) 2026 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Events\Model\CurrencyExchangeRate;
|
||||
|
||||
use FireflyIII\Events\Event;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Models\UserGroup;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class DestroyedCurrencyExchangeRate extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public TransactionCurrency $from,
|
||||
public TransactionCurrency $to,
|
||||
public UserGroup $userGroup
|
||||
) {
|
||||
Log::debug(sprintf('DestroyedCurrencyExchangeRate(%s, %s) Event', $from->code, $to->code));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* UpdatedCurrencyExchangeRate.php
|
||||
* Copyright (c) 2026 james@firefly-iii.org
|
||||
*
|
||||
* This file is part of Firefly III (https://github.com/firefly-iii).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace FireflyIII\Events\Model\CurrencyExchangeRate;
|
||||
|
||||
use FireflyIII\Events\Event;
|
||||
use FireflyIII\Models\CurrencyExchangeRate;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class UpdatedCurrencyExchangeRate extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public CurrencyExchangeRate $rate
|
||||
) {
|
||||
Log::debug(sprintf('UpdatedCurrencyExchangeRate(#%d) Event', $rate->id));
|
||||
}
|
||||
}
|
||||
@@ -251,7 +251,7 @@ class Handler extends ExceptionHandler
|
||||
'method' => request()->method(),
|
||||
'headers' => $headers,
|
||||
// @mago-expect lint:no-request-all
|
||||
'post' => 'POST' === request()->method() ? json_encode(request()->all()) : '',
|
||||
'post' => 'PUT' === request()->method() || 'POST' === request()->method() ? json_encode(request()->all()) : '',
|
||||
];
|
||||
|
||||
// create job that will mail.
|
||||
|
||||
@@ -75,8 +75,6 @@ class GroupCollector implements GroupCollectorInterface
|
||||
$this->userGroup = null;
|
||||
$this->limit = null;
|
||||
$this->page = null;
|
||||
$this->startRow = null;
|
||||
$this->endRow = null;
|
||||
|
||||
$this->hasAccountInfo = false;
|
||||
$this->hasCatInformation = false;
|
||||
@@ -443,9 +441,15 @@ class GroupCollector implements GroupCollectorInterface
|
||||
$this->query->orWhereIn('transaction_journals.transaction_group_id', $groupIds);
|
||||
}
|
||||
$result = $this->query->get($this->fields);
|
||||
$this->total = $result->count();
|
||||
// if no post-filters are present, it can be sliced and returned.
|
||||
if (0 === count($this->sorting) && 0 === count($this->postFilters) && null !== $this->limit && null !== $this->page) {
|
||||
$offset = ($this->page - 1) * $this->limit;
|
||||
$result = $result->slice($offset, $this->limit);
|
||||
}
|
||||
|
||||
// $this->dumpQueryInLogs();
|
||||
// Log::debug(sprintf('Count of result is %d', $result->count()));
|
||||
// now to parse this into an array.
|
||||
// now to parse the rest into an array.
|
||||
$collection = $this->parseArray($result);
|
||||
|
||||
// filter the array using all available post filters:
|
||||
@@ -454,19 +458,12 @@ class GroupCollector implements GroupCollectorInterface
|
||||
// sort the collection, if sort instructions are present.
|
||||
$collection = $this->sortCollection($collection);
|
||||
|
||||
// count it and continue:
|
||||
$this->total = $collection->count();
|
||||
|
||||
// now filter the array according to the page and the limit (if necessary)
|
||||
if (null !== $this->limit && null !== $this->page) {
|
||||
if (count($this->postFilters) > 0 && null !== $this->limit && null !== $this->page) {
|
||||
$offset = ($this->page - 1) * $this->limit;
|
||||
|
||||
return $collection->slice($offset, $this->limit);
|
||||
}
|
||||
// OR filter the array according to the start and end row variable
|
||||
if (null !== $this->startRow && null !== $this->endRow) {
|
||||
return $collection->slice($this->startRow, $this->endRow);
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
@@ -477,17 +474,11 @@ class GroupCollector implements GroupCollectorInterface
|
||||
public function getPaginatedGroups(): LengthAwarePaginator
|
||||
{
|
||||
Log::debug('Now in getPaginatedGroups()');
|
||||
$set = $this->getGroups();
|
||||
$limit = $this->limit ?? 1;
|
||||
if (0 === $this->limit) {
|
||||
$this->setLimit(50);
|
||||
}
|
||||
if (null !== $this->startRow && null !== $this->endRow) {
|
||||
/** @var int $total */
|
||||
$total = $this->endRow - $this->startRow;
|
||||
|
||||
return new LengthAwarePaginator($set, $this->total, $total, 1);
|
||||
}
|
||||
$limit = $this->limit ?? 1;
|
||||
$set = $this->getGroups();
|
||||
|
||||
return new LengthAwarePaginator($set, $this->total, $limit, $this->page);
|
||||
}
|
||||
@@ -519,13 +510,6 @@ class GroupCollector implements GroupCollectorInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setEndRow(int $endRow): self
|
||||
{
|
||||
$this->endRow = $endRow;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setExpandGroupSearch(bool $expandGroupSearch): GroupCollectorInterface
|
||||
{
|
||||
$this->expandGroupSearch = $expandGroupSearch;
|
||||
@@ -636,13 +620,6 @@ class GroupCollector implements GroupCollectorInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setStartRow(int $startRow): self
|
||||
{
|
||||
$this->startRow = $startRow;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit the search to one specific transaction group.
|
||||
*/
|
||||
@@ -692,6 +669,10 @@ class GroupCollector implements GroupCollectorInterface
|
||||
#[Override]
|
||||
public function sortCollection(Collection $collection): Collection
|
||||
{
|
||||
if (0 === count($this->sorting)) {
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var string $field
|
||||
* @var string $direction
|
||||
|
||||
@@ -469,11 +469,6 @@ interface GroupCollectorInterface
|
||||
*/
|
||||
public function setEnd(Carbon $end): self;
|
||||
|
||||
/**
|
||||
* Set the page to get.
|
||||
*/
|
||||
public function setEndRow(int $endRow): self;
|
||||
|
||||
public function setExpandGroupSearch(bool $expandGroupSearch): self;
|
||||
|
||||
/**
|
||||
@@ -573,11 +568,6 @@ interface GroupCollectorInterface
|
||||
*/
|
||||
public function setStart(Carbon $start): self;
|
||||
|
||||
/**
|
||||
* Set the page to get.
|
||||
*/
|
||||
public function setStartRow(int $startRow): self;
|
||||
|
||||
/**
|
||||
* Limit results to a specific tag.
|
||||
*/
|
||||
|
||||
@@ -132,7 +132,7 @@ final class NotificationController extends Controller
|
||||
return redirect(route('settings.notification.index'));
|
||||
}
|
||||
|
||||
$all = $request->only(['channel']);
|
||||
$all = $request->only(['test_submit']);
|
||||
$channel = $all['test_submit'] ?? '';
|
||||
|
||||
switch ($channel) {
|
||||
|
||||
@@ -83,7 +83,7 @@ final class RegisterController extends Controller
|
||||
throw new FireflyException('Registration is currently not available :(');
|
||||
}
|
||||
|
||||
$this->validator($request->only(['email', 'password']))->validate();
|
||||
$this->validator($request->only(['email', 'password', 'password_confirmation']))->validate();
|
||||
$user = $this->createUser($request->only(['email', 'password']));
|
||||
Log::info(sprintf('Registered new user %s', $user->email));
|
||||
$owner = new OwnerNotifiable();
|
||||
|
||||
@@ -44,6 +44,7 @@ use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
@@ -108,7 +109,7 @@ final class DebugController extends Controller
|
||||
Preferences::mark();
|
||||
$request->session()->forget(['start', 'end', '_previous', 'viewRange', 'range', 'is_custom_range', 'temp-mfa-secret', 'temp-mfa-codes']);
|
||||
|
||||
Artisan::call('cache:clear');
|
||||
Cache::clear();
|
||||
Artisan::call('config:clear');
|
||||
Artisan::call('route:clear');
|
||||
Artisan::call('view:clear');
|
||||
|
||||
71
app/Http/Middleware/IsAdminApi.php
Normal file
71
app/Http/Middleware/IsAdminApi.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* IsAdmin.php
|
||||
* Copyright (c) 2019 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/>.
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use FireflyIII\Repositories\User\UserRepositoryInterface;
|
||||
use FireflyIII\User;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class IsAdmin.
|
||||
*/
|
||||
class IsAdminApi
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request. Must be admin.
|
||||
*
|
||||
* @param null|string $guard
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, $guard = null)
|
||||
{
|
||||
if (Auth::guard($guard)->guest()) {
|
||||
if ($request->ajax()) {
|
||||
return response('Unauthorized.', 401);
|
||||
}
|
||||
|
||||
return response()->redirectTo(route('login'));
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
/** @var UserRepositoryInterface $repository */
|
||||
$repository = app(UserRepositoryInterface::class);
|
||||
if (!$repository->hasRole($user, 'owner')) {
|
||||
Log::error(sprintf('Cannot access %s?%s.', $request->url(), $request->getQueryString()));
|
||||
|
||||
throw new AuthorizationException();
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ class UpdatesAccountInformation implements ShouldQueue
|
||||
/** @var RuleAction $action */
|
||||
foreach ($rule->ruleActions as $action) {
|
||||
// fix name:
|
||||
if ($oldData['name'] === $action->action_value && in_array($action->action_type, $fields, true)) {
|
||||
if (array_key_exists('name', $oldData) && $oldData['name'] === $action->action_value && in_array($action->action_type, $fields, true)) {
|
||||
Log::debug(sprintf('Rule action #%d "%s" has old account name, replace with new.', $action->id, $action->action_type));
|
||||
$action->action_value = $account->name;
|
||||
$action->save();
|
||||
@@ -105,21 +105,25 @@ class UpdatesAccountInformation implements ShouldQueue
|
||||
/** @var RuleTrigger $trigger */
|
||||
foreach ($rule->ruleTriggers as $trigger) {
|
||||
// fix name:
|
||||
if ($oldData['name'] === $trigger->trigger_value && in_array($trigger->trigger_type, $nameFields, true)) {
|
||||
if (array_key_exists('name', $oldData) && $oldData['name'] === $trigger->trigger_value && in_array($trigger->trigger_type, $nameFields, true)) {
|
||||
Log::debug(sprintf('Rule trigger #%d "%s" has old account name, replace with new.', $trigger->id, $trigger->trigger_type));
|
||||
$trigger->trigger_value = $account->name;
|
||||
$trigger->save();
|
||||
++$fixed;
|
||||
}
|
||||
// fix IBAN:
|
||||
if ($oldData['iban'] === $trigger->trigger_value && in_array($trigger->trigger_type, $numberFields, true)) {
|
||||
if (array_key_exists('iban', $oldData) && $oldData['iban'] === $trigger->trigger_value && in_array($trigger->trigger_type, $numberFields, true)) {
|
||||
Log::debug(sprintf('Rule trigger #%d "%s" has old account IBAN, replace with new.', $trigger->id, $trigger->trigger_type));
|
||||
$trigger->trigger_value = $account->iban;
|
||||
$trigger->save();
|
||||
++$fixed;
|
||||
}
|
||||
// fix account number: // account_number
|
||||
if ($oldData['account_number'] === $trigger->trigger_value && in_array($trigger->trigger_type, $numberFields, true)) {
|
||||
if (
|
||||
array_key_exists('account_number', $oldData)
|
||||
&& $oldData['account_number'] === $trigger->trigger_value
|
||||
&& in_array($trigger->trigger_type, $numberFields, true)
|
||||
) {
|
||||
Log::debug(sprintf('Rule trigger #%d "%s" has old account account_number, replace with new.', $trigger->id, $trigger->trigger_type));
|
||||
$trigger->trigger_value = $account->iban;
|
||||
$trigger->save();
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* ProcessesExchangeRates.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\Listeners\Model\CurrencyExchangeRate;
|
||||
|
||||
use FireflyIII\Events\Model\CurrencyExchangeRate\CreatedCurrencyExchangeRate;
|
||||
use FireflyIII\Events\Model\CurrencyExchangeRate\DestroyedCurrencyExchangeRate;
|
||||
use FireflyIII\Events\Model\CurrencyExchangeRate\UpdatedCurrencyExchangeRate;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Models\UserGroup;
|
||||
use FireflyIII\Services\Internal\Recalculate\PrimaryAmountRecalculationService;
|
||||
use FireflyIII\Support\Facades\Amount;
|
||||
use FireflyIII\Support\Facades\Preferences;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ProcessesExchangeRates
|
||||
{
|
||||
public function handle(CreatedCurrencyExchangeRate|DestroyedCurrencyExchangeRate|UpdatedCurrencyExchangeRate $event): void
|
||||
{
|
||||
Preferences::mark();
|
||||
Cache::clear();
|
||||
if ($event instanceof DestroyedCurrencyExchangeRate) {
|
||||
$this->handleCurrency($event->userGroup, $event->from);
|
||||
$this->handleCurrency($event->userGroup, $event->to);
|
||||
|
||||
return;
|
||||
}
|
||||
$this->handleCurrency($event->rate->userGroup, $event->rate->fromCurrency);
|
||||
$this->handleCurrency($event->rate->userGroup, $event->rate->toCurrency);
|
||||
}
|
||||
|
||||
private function handleCurrency(UserGroup $userGroup, TransactionCurrency $currency): void
|
||||
{
|
||||
$calculator = new PrimaryAmountRecalculationService();
|
||||
if (Amount::convertToPrimary()) {
|
||||
Log::debug(sprintf('Will now convert amounts to primary currency for currency %s.', $currency->code));
|
||||
|
||||
$calculator->recalculateForGroupAndCurrency($userGroup, $currency);
|
||||
// $calculator->recalculateForGroup($userGroup);
|
||||
|
||||
return;
|
||||
}
|
||||
Log::debug('Will NOT convert to primary currency.');
|
||||
}
|
||||
}
|
||||
@@ -25,46 +25,17 @@ declare(strict_types=1);
|
||||
namespace FireflyIII\Listeners\System;
|
||||
|
||||
use FireflyIII\Events\Preferences\UserGroupChangedPrimaryCurrency;
|
||||
use FireflyIII\Models\Budget;
|
||||
use FireflyIII\Models\PiggyBank;
|
||||
use FireflyIII\Models\UserGroup;
|
||||
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
|
||||
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
|
||||
use FireflyIII\Services\Internal\Recalculate\PrimaryAmountRecalculationService;
|
||||
use FireflyIII\Support\Facades\Amount;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class RecalculatesPrimaryCurrencyAmounts
|
||||
{
|
||||
public function handle(UserGroupChangedPrimaryCurrency $event): void
|
||||
{
|
||||
// Reset the primary currency amounts for all objects that have it.
|
||||
Log::debug('Resetting primary currency amounts for all objects.');
|
||||
|
||||
$tables = [
|
||||
// !!! this array is also in the migration
|
||||
'accounts' => ['native_virtual_balance'],
|
||||
'available_budgets' => ['native_amount'],
|
||||
'bills' => ['native_amount_min', 'native_amount_max'],
|
||||
];
|
||||
foreach ($tables as $table => $columns) {
|
||||
Log::debug(sprintf('Now processing table "%s"', $table));
|
||||
foreach ($columns as $column) {
|
||||
Log::debug(sprintf('Resetting column "%s" in table "%s".', $column, $table));
|
||||
DB::table($table)->where('user_group_id', $event->userGroup->id)->update([$column => null]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->resetPiggyBanks($event->userGroup);
|
||||
$this->resetBudgets($event->userGroup);
|
||||
$this->resetTransactions($event->userGroup);
|
||||
Log::debug('Have now reset all primary amounts to NULL.');
|
||||
// fire laravel command to recalculate them all.
|
||||
if (Amount::convertToPrimary()) {
|
||||
Log::debug('Will now convert amounts to primary currency.');
|
||||
|
||||
$calculator = new PrimaryAmountRecalculationService();
|
||||
$calculator->recalculate();
|
||||
|
||||
@@ -72,87 +43,4 @@ class RecalculatesPrimaryCurrencyAmounts
|
||||
}
|
||||
Log::debug('Will NOT convert to primary currency.');
|
||||
}
|
||||
|
||||
private function resetBudget(Budget $budget): void
|
||||
{
|
||||
foreach ($budget->autoBudgets as $autoBudget) {
|
||||
if ('' === (string) $autoBudget->native_amount) {
|
||||
continue;
|
||||
}
|
||||
Log::debug(sprintf('Resetting native_amount for budget #%d and auto budget #%d.', $budget->id, $autoBudget->id));
|
||||
$autoBudget->native_amount = null;
|
||||
$autoBudget->saveQuietly();
|
||||
}
|
||||
foreach ($budget->budgetlimits as $limit) {
|
||||
if ('' !== (string) $limit->native_amount) {
|
||||
Log::debug(sprintf('Resetting native_amount for budget #%d and budget limit #%d.', $budget->id, $limit->id));
|
||||
$limit->native_amount = null;
|
||||
$limit->saveQuietly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function resetBudgets(UserGroup $userGroup): void
|
||||
{
|
||||
$repository = app(BudgetRepositoryInterface::class);
|
||||
$repository->setUserGroup($userGroup);
|
||||
$set = $repository->getBudgets();
|
||||
|
||||
Log::debug(sprintf('Reset primary currency of %d budget(s).', $set->count()));
|
||||
|
||||
/** @var Budget $budget */
|
||||
foreach ($set as $budget) {
|
||||
$this->resetBudget($budget);
|
||||
}
|
||||
}
|
||||
|
||||
private function resetPiggyBank(PiggyBank $piggyBank): void
|
||||
{
|
||||
if ('' !== (string) $piggyBank->native_target_amount) {
|
||||
Log::debug(sprintf('Resetting native_target_amount for piggy bank #%d.', $piggyBank->id));
|
||||
$piggyBank->native_target_amount = null;
|
||||
$piggyBank->saveQuietly();
|
||||
}
|
||||
foreach ($piggyBank->accounts as $account) {
|
||||
if ('' !== (string) $account->pivot->native_current_amount) {
|
||||
Log::debug(sprintf('Resetting native_current_amount for piggy bank #%d and account #%d.', $piggyBank->id, $account->id));
|
||||
$account->pivot->native_current_amount = null;
|
||||
$account->pivot->save();
|
||||
}
|
||||
}
|
||||
foreach ($piggyBank->piggyBankEvents as $event) {
|
||||
if ('' !== (string) $event->native_amount) {
|
||||
Log::debug(sprintf('Resetting native_amount for piggy bank #%d and event #%d.', $piggyBank->id, $event->id));
|
||||
$event->native_amount = null;
|
||||
$event->saveQuietly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function resetPiggyBanks(UserGroup $userGroup): void
|
||||
{
|
||||
$repository = app(PiggyBankRepositoryInterface::class);
|
||||
$repository->setUserGroup($userGroup);
|
||||
$piggyBanks = $repository->getPiggyBanks();
|
||||
Log::debug(sprintf('Reset primary currency of %d piggy bank(s).', $piggyBanks->count()));
|
||||
|
||||
/** @var PiggyBank $piggyBank */
|
||||
foreach ($piggyBanks as $piggyBank) {
|
||||
$this->resetPiggyBank($piggyBank);
|
||||
}
|
||||
}
|
||||
|
||||
private function resetTransactions(UserGroup $userGroup): void
|
||||
{
|
||||
// custom query because of the potential size of this update.
|
||||
$success = DB::table('transactions')
|
||||
->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||
->where('transaction_journals.user_group_id', $userGroup->id)
|
||||
->where(static function (Builder $q): void {
|
||||
$q->whereNotNull('native_amount')->orWhereNotNull('native_foreign_amount');
|
||||
})
|
||||
->update(['native_amount' => null, 'native_foreign_amount' => null])
|
||||
;
|
||||
Log::debug(sprintf('Reset %d transactions.', $success));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,11 @@ class CurrencyExchangeRate extends Model
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function userGroup(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(UserGroup::class);
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -368,6 +368,15 @@ class BudgetRepository implements BudgetRepositoryInterface, UserGroupInterface
|
||||
|
||||
public function getBudgets(): Collection
|
||||
{
|
||||
if (null === $this->user) {
|
||||
return $this->userGroup
|
||||
->budgets()
|
||||
->orderBy('order', 'ASC')
|
||||
->orderBy('name', 'ASC')
|
||||
->get()
|
||||
;
|
||||
}
|
||||
|
||||
return $this->user
|
||||
->budgets()
|
||||
->orderBy('order', 'ASC')
|
||||
|
||||
@@ -24,6 +24,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace FireflyIII\Services\Internal\Recalculate;
|
||||
|
||||
use FireflyIII\Events\Model\Account\UpdatedExistingAccount;
|
||||
use FireflyIII\Handlers\Observer\TransactionObserver;
|
||||
use FireflyIII\Models\Account;
|
||||
use FireflyIII\Models\AutoBudget;
|
||||
@@ -36,14 +37,16 @@ use FireflyIII\Models\PiggyBankEvent;
|
||||
use FireflyIII\Models\Transaction;
|
||||
use FireflyIII\Models\TransactionCurrency;
|
||||
use FireflyIII\Models\UserGroup;
|
||||
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
|
||||
use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
|
||||
use FireflyIII\Repositories\UserGroup\UserGroupRepositoryInterface;
|
||||
use FireflyIII\Support\Facades\Amount;
|
||||
use FireflyIII\Support\Facades\FireflyConfig;
|
||||
use FireflyIII\Support\Facades\Preferences;
|
||||
use FireflyIII\Support\Http\Api\ExchangeRateConverter;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Query\Builder as DatabaseBuilder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -57,14 +60,52 @@ class PrimaryAmountRecalculationService
|
||||
|
||||
/** @var UserGroupRepositoryInterface $repository */
|
||||
$repository = app(UserGroupRepositoryInterface::class);
|
||||
Preferences::mark();
|
||||
|
||||
/** @var UserGroup $userGroup */
|
||||
foreach ($repository->getAll() as $userGroup) {
|
||||
Log::debug('Resetting primary currency amounts for all objects.');
|
||||
$this->resetGenericTables($userGroup);
|
||||
$this->resetPiggyBanks($userGroup);
|
||||
$this->resetBudgets($userGroup);
|
||||
$this->resetTransactions($userGroup);
|
||||
Log::debug('Have now reset all primary amounts to NULL.');
|
||||
$this->recalculateForGroup($userGroup);
|
||||
}
|
||||
}
|
||||
|
||||
public function recalculateForGroup(UserGroup $userGroup): void
|
||||
{
|
||||
Log::debug(sprintf('Now recalculating primary amounts for user group #%d', $userGroup->id));
|
||||
|
||||
// do a check with the group's currency so we can skip some stuff.
|
||||
$currency = Amount::getPrimaryCurrencyByUserGroup($userGroup);
|
||||
|
||||
$this->recalculateAccounts($userGroup, $currency);
|
||||
$this->recalculatePiggyBanks($userGroup, $currency);
|
||||
$this->recalculateBudgets($userGroup, $currency);
|
||||
$this->recalculateAvailableBudgets($userGroup, $currency);
|
||||
$this->recalculateBills($userGroup, $currency);
|
||||
$this->calculateTransactions($userGroup, $currency);
|
||||
}
|
||||
|
||||
public function recalculateForGroupAndCurrency(UserGroup $userGroup, TransactionCurrency $limitCurrency): void
|
||||
{
|
||||
// do a check with the group's currency so we can skip some stuff.
|
||||
$currency = Amount::getPrimaryCurrencyByUserGroup($userGroup);
|
||||
if ($limitCurrency->id === $currency->id) {
|
||||
Log::debug(sprintf('Can skip recalculation because user requested the same currencies (%s).', $limitCurrency->code));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->recalculateAccountsForCurrency($userGroup, $currency, $limitCurrency);
|
||||
$this->recalculatePiggyBanks($userGroup, $currency);
|
||||
$this->recalculateBudgets($userGroup, $currency);
|
||||
$this->recalculateAvailableBudgets($userGroup, $currency);
|
||||
$this->recalculateBills($userGroup, $currency);
|
||||
$this->calculateTransactionsForCurrency($userGroup, $currency, $limitCurrency);
|
||||
}
|
||||
|
||||
private function calculateTransactions(UserGroup $userGroup, TransactionCurrency $currency): void
|
||||
{
|
||||
// custom query because of the potential size of this update.
|
||||
@@ -86,7 +127,10 @@ class PrimaryAmountRecalculationService
|
||||
->get(['transactions.id'])
|
||||
;
|
||||
TransactionObserver::$recalculate = false;
|
||||
Log::debug(sprintf('Count of set is %d', $set->count()));
|
||||
foreach ($set as $item) {
|
||||
Log::debug(sprintf('Touch transaction #%d', $item->id));
|
||||
|
||||
// here we are.
|
||||
/** @var null|Transaction $transaction */
|
||||
$transaction = Transaction::find($item->id);
|
||||
@@ -96,13 +140,42 @@ class PrimaryAmountRecalculationService
|
||||
Log::debug(sprintf('Recalculated %d transactions.', $set->count()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Only recalculate accounts that have a virtual balance.
|
||||
* TODO this routine must filter on accounts that are NOT in the userGroup's currency.
|
||||
*/
|
||||
private function recalculateAccounts(UserGroup $userGroup): void
|
||||
private function calculateTransactionsForCurrency(UserGroup $userGroup, TransactionCurrency $currency, TransactionCurrency $limitCurrency): void
|
||||
{
|
||||
$set = $userGroup
|
||||
Log::debug(sprintf('Now in calculateTransactionsForCurrency(#%d, %s, %s)', $userGroup->id, $currency->code, $limitCurrency->code));
|
||||
// custom query because of the potential size of this update.
|
||||
$set = DB::table('transactions')
|
||||
->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||
->where('transaction_journals.user_group_id', $userGroup->id)
|
||||
->where(static function (DatabaseBuilder $q1) use ($currency): void {
|
||||
$q1->where(static function (DatabaseBuilder $q2) use ($currency): void {
|
||||
$q2->whereNot('transactions.transaction_currency_id', $currency->id)->whereNull('transactions.foreign_currency_id');
|
||||
})->orWhere(static function (DatabaseBuilder $q3) use ($currency): void {
|
||||
$q3->whereNot('transactions.transaction_currency_id', $currency->id)->whereNot('transactions.foreign_currency_id', $currency->id);
|
||||
});
|
||||
})
|
||||
// must be in the limit currency.
|
||||
->where('transactions.transaction_currency_id', $limitCurrency->id)
|
||||
->orWhere('transactions.foreign_currency_id', $limitCurrency->id)
|
||||
->get(['transactions.id'])
|
||||
;
|
||||
TransactionObserver::$recalculate = false;
|
||||
Log::debug(sprintf('Count of set is %d', $set->count()));
|
||||
foreach ($set as $item) {
|
||||
Log::debug(sprintf('Touch transaction #%d', $item->id));
|
||||
|
||||
// here we are.
|
||||
/** @var null|Transaction $transaction */
|
||||
$transaction = Transaction::find($item->id);
|
||||
$transaction?->touch();
|
||||
}
|
||||
TransactionObserver::$recalculate = true;
|
||||
Log::debug(sprintf('Recalculated %d transactions.', $set->count()));
|
||||
}
|
||||
|
||||
private function collectAccounts(UserGroup $userGroup): Collection
|
||||
{
|
||||
return $userGroup
|
||||
->accounts()
|
||||
->where(static function (EloquentBuilder $q): void {
|
||||
$q->whereNotNull('virtual_balance');
|
||||
@@ -117,14 +190,59 @@ class PrimaryAmountRecalculationService
|
||||
})
|
||||
->get()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only recalculate accounts that have a virtual balance.
|
||||
*/
|
||||
private function recalculateAccounts(UserGroup $userGroup, TransactionCurrency $groupCurrency): void
|
||||
{
|
||||
Log::debug(sprintf('recalculateAccounts(#%d, %s)', $userGroup->id, $groupCurrency->code));
|
||||
$set = $this->collectAccounts($userGroup);
|
||||
|
||||
/** @var Account $account */
|
||||
foreach ($set as $account) {
|
||||
$currencyId = (int) $account->accountMeta()->where('name', 'currency_id')->first()?->data;
|
||||
if ($groupCurrency->id === $currencyId) {
|
||||
Log::debug(sprintf('Account "%s" is in group currency %s. Skip.', $account->name, $groupCurrency->code));
|
||||
|
||||
continue;
|
||||
}
|
||||
Log::debug(sprintf('Account "%s" is NOT in group currency %s, so do it.', $account->name, $groupCurrency->code));
|
||||
$account->touch();
|
||||
}
|
||||
Log::debug(sprintf('Recalculated %d accounts for user group #%d.', $set->count(), $userGroup->id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Only recalculate accounts that have a virtual balance.
|
||||
*/
|
||||
private function recalculateAccountsForCurrency(UserGroup $userGroup, TransactionCurrency $groupCurrency, TransactionCurrency $limitCurrency): void
|
||||
{
|
||||
Log::debug(sprintf('recalculateAccountsForCurrency(#%d, %s, %s)', $userGroup->id, $groupCurrency->code, $limitCurrency->code));
|
||||
|
||||
$set = $this->collectAccounts($userGroup);
|
||||
|
||||
/** @var Account $account */
|
||||
foreach ($set as $account) {
|
||||
$currencyId = (int) $account->accountMeta()->where('name', 'currency_id')->first()->data;
|
||||
if ($groupCurrency->id === $currencyId) {
|
||||
Log::debug(sprintf('Account "%s" is in group currency %s. Skip.', $account->name, $groupCurrency->code));
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($limitCurrency->id !== $currencyId) {
|
||||
Log::debug(sprintf('Account "%s" is NOT in limit currency %s, skip.', $account->name, $limitCurrency->code));
|
||||
|
||||
continue;
|
||||
}
|
||||
Log::debug(sprintf('Account "%s" is NOT in group currency %s, so do it.', $account->name, $groupCurrency->code));
|
||||
// TODO it is bad form to call an event from an event but OK.
|
||||
event(new UpdatedExistingAccount($account, []));
|
||||
}
|
||||
Log::debug(sprintf('Recalculated %d accounts for user group #%d.', $set->count(), $userGroup->id));
|
||||
}
|
||||
|
||||
private function recalculateAutoBudgets(Budget $budget, TransactionCurrency $currency): void
|
||||
{
|
||||
$set = $budget->autoBudgets()->where('transaction_currency_id', '!=', $currency->id)->get();
|
||||
@@ -184,21 +302,6 @@ class PrimaryAmountRecalculationService
|
||||
Log::debug(sprintf('Recalculated %d budgets.', $set->count()));
|
||||
}
|
||||
|
||||
private function recalculateForGroup(UserGroup $userGroup): void
|
||||
{
|
||||
Log::debug(sprintf('Now recalculating primary amounts for user group #%d', $userGroup->id));
|
||||
$this->recalculateAccounts($userGroup);
|
||||
|
||||
// do a check with the group's currency so we can skip some stuff.
|
||||
$currency = Amount::getPrimaryCurrencyByUserGroup($userGroup);
|
||||
|
||||
$this->recalculatePiggyBanks($userGroup, $currency);
|
||||
$this->recalculateBudgets($userGroup, $currency);
|
||||
$this->recalculateAvailableBudgets($userGroup, $currency);
|
||||
$this->recalculateBills($userGroup, $currency);
|
||||
$this->calculateTransactions($userGroup, $currency);
|
||||
}
|
||||
|
||||
private function recalculatePiggyBankEvents(PiggyBank $piggyBank): void
|
||||
{
|
||||
$set = $piggyBank->piggyBankEvents()->get();
|
||||
@@ -240,4 +343,104 @@ class PrimaryAmountRecalculationService
|
||||
}
|
||||
Log::debug(sprintf('Recalculated %d piggy banks for user group #%d.', $set->count(), $userGroup->id));
|
||||
}
|
||||
|
||||
private function resetBudget(Budget $budget): void
|
||||
{
|
||||
foreach ($budget->autoBudgets as $autoBudget) {
|
||||
if ('' === (string) $autoBudget->native_amount) {
|
||||
continue;
|
||||
}
|
||||
Log::debug(sprintf('Resetting native_amount for budget #%d and auto budget #%d.', $budget->id, $autoBudget->id));
|
||||
$autoBudget->native_amount = null;
|
||||
$autoBudget->saveQuietly();
|
||||
}
|
||||
foreach ($budget->budgetlimits as $limit) {
|
||||
if ('' !== (string) $limit->native_amount) {
|
||||
Log::debug(sprintf('Resetting native_amount for budget #%d and budget limit #%d.', $budget->id, $limit->id));
|
||||
$limit->native_amount = null;
|
||||
$limit->saveQuietly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function resetBudgets(UserGroup $userGroup): void
|
||||
{
|
||||
$repository = app(BudgetRepositoryInterface::class);
|
||||
$repository->setUserGroup($userGroup);
|
||||
$set = $repository->getBudgets();
|
||||
|
||||
Log::debug(sprintf('Reset primary currency of %d budget(s).', $set->count()));
|
||||
|
||||
/** @var Budget $budget */
|
||||
foreach ($set as $budget) {
|
||||
$this->resetBudget($budget);
|
||||
}
|
||||
}
|
||||
|
||||
private function resetGenericTables(UserGroup $userGroup): void
|
||||
{
|
||||
$tables = [
|
||||
// !!! this array is also in the migration
|
||||
'accounts' => ['native_virtual_balance'],
|
||||
'available_budgets' => ['native_amount'],
|
||||
'bills' => ['native_amount_min', 'native_amount_max'],
|
||||
];
|
||||
foreach ($tables as $table => $columns) {
|
||||
Log::debug(sprintf('Now processing table "%s"', $table));
|
||||
foreach ($columns as $column) {
|
||||
Log::debug(sprintf('Resetting column "%s" in table "%s".', $column, $table));
|
||||
DB::table($table)->where('user_group_id', $userGroup->id)->update([$column => null]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function resetPiggyBank(PiggyBank $piggyBank): void
|
||||
{
|
||||
if ('' !== (string) $piggyBank->native_target_amount) {
|
||||
Log::debug(sprintf('Resetting native_target_amount for piggy bank #%d.', $piggyBank->id));
|
||||
$piggyBank->native_target_amount = null;
|
||||
$piggyBank->saveQuietly();
|
||||
}
|
||||
foreach ($piggyBank->accounts as $account) {
|
||||
if ('' !== (string) $account->pivot->native_current_amount) {
|
||||
Log::debug(sprintf('Resetting native_current_amount for piggy bank #%d and account #%d.', $piggyBank->id, $account->id));
|
||||
$account->pivot->native_current_amount = null;
|
||||
$account->pivot->save();
|
||||
}
|
||||
}
|
||||
foreach ($piggyBank->piggyBankEvents as $event) {
|
||||
if ('' !== (string) $event->native_amount) {
|
||||
Log::debug(sprintf('Resetting native_amount for piggy bank #%d and event #%d.', $piggyBank->id, $event->id));
|
||||
$event->native_amount = null;
|
||||
$event->saveQuietly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function resetPiggyBanks(UserGroup $userGroup): void
|
||||
{
|
||||
$repository = app(PiggyBankRepositoryInterface::class);
|
||||
$repository->setUserGroup($userGroup);
|
||||
$piggyBanks = $repository->getPiggyBanks();
|
||||
Log::debug(sprintf('Reset primary currency of %d piggy bank(s).', $piggyBanks->count()));
|
||||
|
||||
/** @var PiggyBank $piggyBank */
|
||||
foreach ($piggyBanks as $piggyBank) {
|
||||
$this->resetPiggyBank($piggyBank);
|
||||
}
|
||||
}
|
||||
|
||||
private function resetTransactions(UserGroup $userGroup): void
|
||||
{
|
||||
// custom query because of the potential size of this update.
|
||||
$success = DB::table('transactions')
|
||||
->join('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
|
||||
->where('transaction_journals.user_group_id', $userGroup->id)
|
||||
->where(static function (Builder $q): void {
|
||||
$q->whereNotNull('native_amount')->orWhereNotNull('native_foreign_amount');
|
||||
})
|
||||
->update(['native_amount' => null, 'native_foreign_amount' => null])
|
||||
;
|
||||
Log::debug(sprintf('Reset %d transactions.', $success));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class Date implements BinderInterface
|
||||
try {
|
||||
$result = new Carbon($value);
|
||||
} catch (InvalidDateException|InvalidFormatException $e) {
|
||||
$message = sprintf('Could not parse date "%s" for user #%d: %s', $value, auth()->user()->id, $e->getMessage());
|
||||
$message = sprintf('Could not parse date "%s" for user #%d: %s', $value, (int) auth()->user()?->id, $e->getMessage());
|
||||
Log::error($message);
|
||||
|
||||
throw new NotFoundHttpException('Could not parse value', $e);
|
||||
|
||||
@@ -29,6 +29,7 @@ use FireflyIII\Http\Middleware\EncryptCookies;
|
||||
use FireflyIII\Http\Middleware\Installer;
|
||||
use FireflyIII\Http\Middleware\InterestingMessage;
|
||||
use FireflyIII\Http\Middleware\IsAdmin;
|
||||
use FireflyIII\Http\Middleware\IsAdminApi;
|
||||
use FireflyIII\Http\Middleware\Range;
|
||||
use FireflyIII\Http\Middleware\RedirectIfAuthenticated;
|
||||
use FireflyIII\Http\Middleware\SecureHeaders;
|
||||
@@ -157,7 +158,7 @@ $app = Application::configure(basePath: dirname(__DIR__))
|
||||
// This middleware is added to ensure that the user is not only logged in and
|
||||
// authenticated (with MFA and everything), but also admin.
|
||||
$middleware->appendToGroup('api-admin', [
|
||||
IsAdmin::class,
|
||||
IsAdminApi::class,
|
||||
]);
|
||||
$middleware->appendToGroup('admin', [
|
||||
IsAdmin::class,
|
||||
|
||||
32
changelog.md
32
changelog.md
@@ -3,38 +3,32 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## v6.5.7 - 2026-03-xx
|
||||
## v6.5.8 - 2026-03-22
|
||||
|
||||
<!-- summary: If you can read this I forgot to update the summary! -->
|
||||
<!-- summary: This release fixes a regression bug in user registration. -->
|
||||
|
||||
### Added
|
||||
### Fixed
|
||||
|
||||
- Initial release.
|
||||
- [Issue 11995](https://github.com/firefly-iii/firefly-iii/issues/11995) (User registration breaks on password validation) reported by @mikaelhm
|
||||
|
||||
### Changed
|
||||
## v6.5.7 - 2026-03-21
|
||||
|
||||
- Initial release.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Initial release.
|
||||
|
||||
### Removed
|
||||
|
||||
- Initial release.
|
||||
<!-- summary: There is a new security policy for AI-generated security advisories and of course, some interesting but annoying bugs fixed. -->
|
||||
|
||||
### Fixed
|
||||
|
||||
- [Issue 11964](https://github.com/firefly-iii/firefly-iii/issues/11964) ("Left to spend" is not taking into account non-main currency withdrawals (when displaying in primary currency)) reported by @absdjfh
|
||||
- [Issue 11966](https://github.com/firefly-iii/firefly-iii/issues/11966) (Error when trying to export data in CSV (Export data via front end)) reported by @jgmm81
|
||||
|
||||
### Security
|
||||
|
||||
- Initial release.
|
||||
- [Issue 11969](https://github.com/firefly-iii/firefly-iii/issues/11969) (Problem found when editing a multi-currency record, as well as details in the "Audit log entries") reported by @jgmm81
|
||||
- [PR 11974](https://github.com/firefly-iii/firefly-iii/pull/11974) (Fix typo in SMTP server comment in .env.example) reported by @NorskNoobing
|
||||
- [Discussion 11977](https://github.com/orgs/firefly-iii/discussions/11977) (CSP header `form-action 'self'` prevents form submission because it's a redirect) started by @superrio0187
|
||||
- [Issue 11978](https://github.com/firefly-iii/firefly-iii/issues/11978) (Tags not associated with any record display incorrect information) reported by @jgmm81
|
||||
- [Issue 11982](https://github.com/firefly-iii/firefly-iii/issues/11982) (Foreign currency account value in primary currency does not update after changing exchange rates) reported by @gattacus
|
||||
- Remove old `zoomLevel` / `zoom_level` database references for tags, since they are no longer queries anyway.
|
||||
|
||||
### API
|
||||
|
||||
- Initial release.
|
||||
- [Issue 11976](https://github.com/firefly-iii/firefly-iii/issues/11976) (New lines are removed from rule description when created using API POST) reported by @AlexRNL
|
||||
|
||||
## v6.5.6 - 2026-03-16
|
||||
|
||||
|
||||
18
composer.lock
generated
18
composer.lock
generated
@@ -5781,27 +5781,27 @@
|
||||
},
|
||||
{
|
||||
"name": "rcrowe/twigbridge",
|
||||
"version": "v0.14.6",
|
||||
"version": "v0.14.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/rcrowe/TwigBridge.git",
|
||||
"reference": "0798ee4b5e5b943d0200850acaa87ccd82e2fe45"
|
||||
"reference": "03a767c8d5c1d74d5f14e9fc754619a271822663"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/rcrowe/TwigBridge/zipball/0798ee4b5e5b943d0200850acaa87ccd82e2fe45",
|
||||
"reference": "0798ee4b5e5b943d0200850acaa87ccd82e2fe45",
|
||||
"url": "https://api.github.com/repos/rcrowe/TwigBridge/zipball/03a767c8d5c1d74d5f14e9fc754619a271822663",
|
||||
"reference": "03a767c8d5c1d74d5f14e9fc754619a271822663",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/support": "^9|^10|^11|^12",
|
||||
"illuminate/view": "^9|^10|^11|^12",
|
||||
"illuminate/support": "^9|^10|^11|^12|^13",
|
||||
"illuminate/view": "^9|^10|^11|^12|^13",
|
||||
"php": "^8.1",
|
||||
"twig/twig": "~3.21"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-json": "*",
|
||||
"laravel/framework": "^9|^10|^11|^12",
|
||||
"laravel/framework": "^9|^10|^11|^12|^13",
|
||||
"mockery/mockery": "^1.3.1",
|
||||
"phpunit/phpunit": "^8.5.8 || ^9.3.7 || ^10.0 || ^11.0 || ^12.0",
|
||||
"squizlabs/php_codesniffer": "^3.6"
|
||||
@@ -5847,9 +5847,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/rcrowe/TwigBridge/issues",
|
||||
"source": "https://github.com/rcrowe/TwigBridge/tree/v0.14.6"
|
||||
"source": "https://github.com/rcrowe/TwigBridge/tree/v0.14.7"
|
||||
},
|
||||
"time": "2025-08-20T11:25:49+00:00"
|
||||
"time": "2026-03-20T16:59:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "spatie/backtrace",
|
||||
|
||||
@@ -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-20',
|
||||
'build_time' => 1773991310,
|
||||
'version' => 'develop/2026-03-22',
|
||||
'build_time' => 1774166412,
|
||||
'api_version' => '2.1.0', // field is no longer used.
|
||||
'db_version' => 28, // field is no longer used.
|
||||
|
||||
|
||||
1331
package-lock.json
generated
1331
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,10 +9,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^1",
|
||||
"laravel-vite-plugin": "^2",
|
||||
"laravel-vite-plugin": "^3",
|
||||
"patch-package": "^8",
|
||||
"sass": "^1",
|
||||
"vite": "^7",
|
||||
"vite": "^8",
|
||||
"vite-plugin-manifest-sri": "^0.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -20,6 +20,14 @@
|
||||
|
||||
export default class GenericObjectRenderer {
|
||||
renderUrl(url, title, text) {
|
||||
return `<a href="${url}" title="${title}">${text}</a>`;
|
||||
return `<a href="${url}" title="${this.escapeHtml(title)}">${this.escapeHtml(text)}</a>`;
|
||||
}
|
||||
escapeHtml(unsafe) {
|
||||
return unsafe
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
};
|
||||
}
|
||||
|
||||
@@ -347,7 +347,6 @@ Route::group(
|
||||
'namespace' => 'FireflyIII\Api\V1\Controllers\Models\UserGroup',
|
||||
'prefix' => 'v1/user-groups',
|
||||
'as' => 'api.v1.user-groups.',
|
||||
'middleware' => ['api-admin'],
|
||||
],
|
||||
static function (): void {
|
||||
Route::get('', ['uses' => 'IndexController@index', 'as' => 'index']);
|
||||
@@ -636,12 +635,10 @@ Route::group(
|
||||
],
|
||||
static function (): void {
|
||||
Route::get('', ['uses' => 'ShowController@index', 'as' => 'index']);
|
||||
Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']);
|
||||
Route::put('{currency_code?}', ['uses' => 'UpdateController@update', 'as' => 'update']);
|
||||
Route::get('primary', ['uses' => 'ShowController@showPrimary', 'as' => 'show.primary']);
|
||||
Route::get('default', ['uses' => 'ShowController@showPrimary', 'as' => 'show.default']);
|
||||
Route::get('{currency_code}', ['uses' => 'ShowController@show', 'as' => 'show']);
|
||||
Route::put('{currency_code?}', ['uses' => 'UpdateController@update', 'as' => 'update']);
|
||||
Route::delete('{currency_code}', ['uses' => 'DestroyController@destroy', 'as' => 'delete']);
|
||||
|
||||
Route::post('{currency_code}/enable', ['uses' => 'UpdateController@enable', 'as' => 'enable']);
|
||||
Route::post('{currency_code}/disable', ['uses' => 'UpdateController@disable', 'as' => 'disable']);
|
||||
@@ -658,6 +655,21 @@ Route::group(
|
||||
}
|
||||
);
|
||||
|
||||
// Transaction currency API routes that require admin rights:
|
||||
Route::group(
|
||||
[
|
||||
'namespace' => 'FireflyIII\Api\V1\Controllers\Models\TransactionCurrency',
|
||||
'prefix' => 'v1/currencies',
|
||||
'as' => 'api.v1.currencies.',
|
||||
'middleware' => ['api-admin'],
|
||||
],
|
||||
static function (): void {
|
||||
Route::delete('{currency_code}', ['uses' => 'DestroyController@destroy', 'as' => 'delete']);
|
||||
Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// Transaction Links API routes:
|
||||
Route::group(
|
||||
[
|
||||
@@ -683,11 +695,23 @@ Route::group(
|
||||
],
|
||||
static function (): void {
|
||||
Route::get('', ['uses' => 'ShowController@index', 'as' => 'index']);
|
||||
Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']);
|
||||
Route::get('{linkType}', ['uses' => 'ShowController@show', 'as' => 'show']);
|
||||
Route::get('{linkType}/transactions', ['uses' => 'ListController@transactions', 'as' => 'transactions']);
|
||||
}
|
||||
);
|
||||
|
||||
// Transaction Link Type API routes that need admin rights.
|
||||
Route::group(
|
||||
[
|
||||
'namespace' => 'FireflyIII\Api\V1\Controllers\Models\TransactionLinkType',
|
||||
'prefix' => 'v1/link-types',
|
||||
'as' => 'api.v1.link-types.',
|
||||
'middleware' => ['api-admin'],
|
||||
],
|
||||
static function (): void {
|
||||
Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']);
|
||||
Route::put('{linkType}', ['uses' => 'UpdateController@update', 'as' => 'update']);
|
||||
Route::delete('{linkType}', ['uses' => 'DestroyController@destroy', 'as' => 'delete']);
|
||||
Route::get('{linkType}/transactions', ['uses' => 'ListController@transactions', 'as' => 'transactions']);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -727,10 +751,23 @@ Route::group(
|
||||
],
|
||||
static function (): void {
|
||||
Route::get('', ['uses' => 'ConfigurationController@index', 'as' => 'index']);
|
||||
Route::put('{dynamicConfigKey}', ['uses' => 'ConfigurationController@update', 'as' => 'update']);
|
||||
Route::get('{eitherConfigKey}', ['uses' => 'ConfigurationController@show', 'as' => 'show']);
|
||||
}
|
||||
);
|
||||
|
||||
// Configuration API routes that need admin rights
|
||||
Route::group(
|
||||
[
|
||||
'namespace' => 'FireflyIII\Api\V1\Controllers\System',
|
||||
'prefix' => 'v1/configuration',
|
||||
'as' => 'api.v1.configuration.',
|
||||
'middleware' => ['api-admin'],
|
||||
],
|
||||
static function (): void {
|
||||
Route::put('{dynamicConfigKey}', ['uses' => 'ConfigurationController@update', 'as' => 'update']);
|
||||
}
|
||||
);
|
||||
|
||||
// Users API routes:
|
||||
Route::group(
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user