diff --git a/app/Api/V1/Controllers/Models/Budget/ListController.php b/app/Api/V1/Controllers/Models/Budget/ListController.php
index 709d092afe..5e5bb38ac6 100644
--- a/app/Api/V1/Controllers/Models/Budget/ListController.php
+++ b/app/Api/V1/Controllers/Models/Budget/ListController.php
@@ -25,6 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Models\Budget;
use FireflyIII\Api\V1\Controllers\Controller;
+use FireflyIII\Enums\TransactionTypeEnum;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
use FireflyIII\Models\Budget;
@@ -208,6 +209,8 @@ class ListController extends Controller
$collector = app(GroupCollectorInterface::class);
$collector
->setUser($admin)
+ // withdrawals only
+ ->setTypes([TransactionTypeEnum::WITHDRAWAL->value])
// filter on budget.
->withoutBudget()
// all info needed for the API:
diff --git a/app/Api/V2/Controllers/Model/ExchangeRate/DestroyController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php
similarity index 95%
rename from app/Api/V2/Controllers/Model/ExchangeRate/DestroyController.php
rename to app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php
index e46cc5375d..ef262dd73c 100644
--- a/app/Api/V2/Controllers/Model/ExchangeRate/DestroyController.php
+++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/DestroyController.php
@@ -2,7 +2,7 @@
/*
* DestroyController.php
- * Copyright (c) 2024 james@firefly-iii.org.
+ * Copyright (c) 2025 james@firefly-iii.org.
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -22,7 +22,7 @@
declare(strict_types=1);
-namespace FireflyIII\Api\V2\Controllers\Model\ExchangeRate;
+namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate;
use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Api\V2\Request\Model\ExchangeRate\DestroyRequest;
diff --git a/app/Api/V2/Controllers/Model/ExchangeRate/IndexController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/IndexController.php
similarity index 89%
rename from app/Api/V2/Controllers/Model/ExchangeRate/IndexController.php
rename to app/Api/V1/Controllers/Models/CurrencyExchangeRate/IndexController.php
index d800678946..108d4cae1b 100644
--- a/app/Api/V2/Controllers/Model/ExchangeRate/IndexController.php
+++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/IndexController.php
@@ -1,8 +1,8 @@
.
+ * along with this program. If not, see https://www.gnu.org/licenses/.
*/
declare(strict_types=1);
-namespace FireflyIII\Api\V2\Controllers\Model\ExchangeRate;
+namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate;
use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Repositories\UserGroups\ExchangeRate\ExchangeRateRepositoryInterface;
@@ -38,7 +38,7 @@ class IndexController extends Controller
{
use ValidatesUserGroupTrait;
- public const string RESOURCE_KEY = 'exchange-rates';
+ public const string RESOURCE_KEY = 'exchange_rates';
private ExchangeRateRepositoryInterface $repository;
diff --git a/app/Api/V2/Controllers/Model/ExchangeRate/ShowController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php
similarity index 93%
rename from app/Api/V2/Controllers/Model/ExchangeRate/ShowController.php
rename to app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php
index 5a4f40c545..a8e2226385 100644
--- a/app/Api/V2/Controllers/Model/ExchangeRate/ShowController.php
+++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/ShowController.php
@@ -2,7 +2,7 @@
/*
* ShowController.php
- * Copyright (c) 2023 james@firefly-iii.org
+ * Copyright (c) 2025 james@firefly-iii.org.
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@@ -17,12 +17,12 @@
* 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 .
+ * along with this program. If not, see https://www.gnu.org/licenses/.
*/
declare(strict_types=1);
-namespace FireflyIII\Api\V2\Controllers\Model\ExchangeRate;
+namespace FireflyIII\Api\V1\Controllers\Models\CurrencyExchangeRate;
use FireflyIII\Api\V2\Controllers\Controller;
use FireflyIII\Models\TransactionCurrency;
diff --git a/app/Api/V2/Controllers/Model/ExchangeRate/StoreController.php b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php
similarity index 95%
rename from app/Api/V2/Controllers/Model/ExchangeRate/StoreController.php
rename to app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php
index 73e14892a7..df9a44796f 100644
--- a/app/Api/V2/Controllers/Model/ExchangeRate/StoreController.php
+++ b/app/Api/V1/Controllers/Models/CurrencyExchangeRate/StoreController.php
@@ -1,8 +1,8 @@
json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
}
- /**
- * TODO This endpoint is not documented.
- *
- * Return a single preference by name.
- *
- * @param Collection $collection
- */
- public function showList(Collection $collection): JsonResponse
- {
- $manager = $this->getManager();
- $count = $collection->count();
- $pageSize = $this->parameters->get('limit');
- $preferences = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
-
- // make paginator:
- $paginator = new LengthAwarePaginator($preferences, $count, $pageSize, $this->parameters->get('page'));
- $paginator->setPath(route('api.v1.preferences.show-list').$this->buildParams());
-
- /** @var PreferenceTransformer $transformer */
- $transformer = app(PreferenceTransformer::class);
- $transformer->setParameters($this->parameters);
-
- $resource = new FractalCollection($preferences, $transformer, self::RESOURCE_KEY);
- $resource->setPaginator(new IlluminatePaginatorAdapter($paginator));
-
- return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
- }
-
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/?urls.primaryName=2.0.0%20(v1)#/preferences/storePreference
diff --git a/app/Api/V1/Requests/Models/Bill/StoreRequest.php b/app/Api/V1/Requests/Models/Bill/StoreRequest.php
index e6b82c2216..7e0ba7bd80 100644
--- a/app/Api/V1/Requests/Models/Bill/StoreRequest.php
+++ b/app/Api/V1/Requests/Models/Bill/StoreRequest.php
@@ -31,6 +31,8 @@ use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Validator;
+use TypeError;
+use ValueError;
/**
* Class StoreRequest
@@ -95,16 +97,38 @@ class StoreRequest extends FormRequest
{
$validator->after(
static function (Validator $validator): void {
- $data = $validator->getData();
- $min = (string) ($data['amount_min'] ?? '0');
- $max = (string) ($data['amount_max'] ?? '0');
+ $data = $validator->getData();
+ $min = $data['amount_min'] ?? '0';
+ $max = $data['amount_max'] ?? '0';
- if (1 === bccomp($min, $max)) {
+ if(is_array($min) || is_array($max)) {
+ $validator->errors()->add('amount_min', (string) trans('validation.generic_invalid'));
+ $validator->errors()->add('amount_max', (string) trans('validation.generic_invalid'));
+ $min ='0';
+ $max = '0';
+ }
+ $result = false;
+ try {
+ $result = bccomp($min, $max);
+ } catch (ValueError $e) {
+ Log::error($e->getMessage());
+ $validator->errors()->add('amount_min', (string) trans('validation.generic_invalid'));
+ $validator->errors()->add('amount_max', (string) trans('validation.generic_invalid'));
+ }
+
+ if (1 === $result) {
$validator->errors()->add('amount_min', (string) trans('validation.amount_min_over_max'));
}
}
);
- if ($validator->fails()) {
+ $failed = false;
+ try {
+ $failed = $validator->fails();
+ } catch (TypeError $e) {
+ Log::error($e->getMessage());
+ $failed = false;
+ }
+ if ($failed) {
Log::channel('audit')->error(sprintf('Validation errors in %s', __CLASS__), $validator->errors()->toArray());
}
}
diff --git a/app/Rules/IsValidPositiveAmount.php b/app/Rules/IsValidPositiveAmount.php
index 233fab613d..0e09647f35 100644
--- a/app/Rules/IsValidPositiveAmount.php
+++ b/app/Rules/IsValidPositiveAmount.php
@@ -38,6 +38,15 @@ class IsValidPositiveAmount implements ValidationRule
*/
public function validate(string $attribute, mixed $value, \Closure $fail): void
{
+ if(is_array($value)) {
+ $fail('validation.numeric')->translate();
+ $message = sprintf('IsValidPositiveAmount: "%s" is not a number.', json_encode($value));
+ Log::debug($message);
+ Log::channel('audit')->info($message);
+
+ return;
+ }
+
$value = (string) $value;
// must not be empty:
if ($this->emptyString($value)) {
diff --git a/app/Transformers/UserGroupTransformer.php b/app/Transformers/UserGroupTransformer.php
index a001ca7c60..95f8a39613 100644
--- a/app/Transformers/UserGroupTransformer.php
+++ b/app/Transformers/UserGroupTransformer.php
@@ -113,14 +113,14 @@ class UserGroupTransformer extends AbstractTransformer
'created_at' => $userGroup->created_at->toAtomString(),
'updated_at' => $userGroup->updated_at->toAtomString(),
'in_use' => $this->inUse[$userGroup->id] ?? false,
- 'title' => $userGroup->title,
'can_see_members' => $this->membershipsVisible[$userGroup->id] ?? false,
- 'members' => array_values($this->memberships[$userGroup->id] ?? []),
+ 'title' => $userGroup->title,
'native_currency_id' => (string) $currency->id,
'native_currency_name' => $currency->name,
'native_currency_code' => $currency->code,
'native_currency_symbol' => $currency->symbol,
'native_currency_decimal_places' => $currency->decimal_places,
+ 'members' => array_values($this->memberships[$userGroup->id] ?? []),
];
// if the user has a specific role in this group, then collect the memberships.
}
diff --git a/resources/assets/v1/src/components/exchange-rates/Index.vue b/resources/assets/v1/src/components/exchange-rates/Index.vue
index b06dd5f683..38f44a84ad 100644
--- a/resources/assets/v1/src/components/exchange-rates/Index.vue
+++ b/resources/assets/v1/src/components/exchange-rates/Index.vue
@@ -68,7 +68,7 @@ export default {
this.downloadCurrencies(1);
},
downloadCurrencies: function (page) {
- axios.get("./api/v2/currencies?enabled=1&page=" + page).then((response) => {
+ axios.get("./api/v1/currencies?enabled=1&page=" + page).then((response) => {
for (let i in response.data.data) {
if (response.data.data.hasOwnProperty(i)) {
let current = response.data.data[i];
diff --git a/resources/assets/v1/src/components/exchange-rates/Rates.vue b/resources/assets/v1/src/components/exchange-rates/Rates.vue
index 502fe7dd6b..3c8250756e 100644
--- a/resources/assets/v1/src/components/exchange-rates/Rates.vue
+++ b/resources/assets/v1/src/components/exchange-rates/Rates.vue
@@ -183,7 +183,7 @@ export default {
if(e) e.preventDefault();
this.posting = true;
- axios.post("./api/v2/exchange-rates", {
+ axios.post("./api/v1/exchange-rates", {
from: this.from_code,
to: this.to_code,
rate: this.newRate,
@@ -214,7 +214,7 @@ export default {
// console.log('Rate is ' + this.rates[index].rate);
// console.log('ID is ' + this.rates[index].rate_id);
this.updating = true;
- axios.put("./api/v2/exchange-rates/" + this.rates[index].rate_id, {rate: this.rates[index].rate})
+ axios.put("./api/v1/exchange-rates/" + this.rates[index].rate_id, {rate: this.rates[index].rate})
.then(() => {
this.updating = false;
});
@@ -224,7 +224,7 @@ export default {
// console.log('Inverse is ' + this.rates[index].inverse);
// console.log('Inverse ID is ' + this.rates[index].inverse_id);
this.updating = true;
- axios.put("./api/v2/exchange-rates/" + this.rates[index].inverse_id, {rate: this.rates[index].inverse})
+ axios.put("./api/v1/exchange-rates/" + this.rates[index].inverse_id, {rate: this.rates[index].inverse})
.then(() => {
this.updating = false;
});
@@ -239,9 +239,9 @@ export default {
// console.log(parts);
// delete A to B
- axios.delete("./api/v2/exchange-rates/rates/" + parts.from + '/' + parts.to + '?date=' + format(parts.date, 'yyyy-MM-dd'));
+ axios.delete("./api/v1/exchange-rates/rates/" + parts.from + '/' + parts.to + '?date=' + format(parts.date, 'yyyy-MM-dd'));
// delete B to A.
- axios.delete("./api/v2/exchange-rates/rates/" + parts.to + '/' + parts.from + '?date=' + format(parts.date, 'yyyy-MM-dd'));
+ axios.delete("./api/v1/exchange-rates/rates/" + parts.to + '/' + parts.from + '?date=' + format(parts.date, 'yyyy-MM-dd'));
this.rates.splice(index, 1);
},
@@ -263,14 +263,14 @@ export default {
},
downloadCurrencies: function () {
this.loading = true;
- axios.get("./api/v2/currencies/" + this.from_code).then((response) => {
+ axios.get("./api/v1/currencies/" + this.from_code).then((response) => {
this.from = {
id: response.data.data.id,
code: response.data.data.attributes.code,
name: response.data.data.attributes.name,
}
});
- axios.get("./api/v2/currencies/" + this.to_code).then((response) => {
+ axios.get("./api/v1/currencies/" + this.to_code).then((response) => {
// console.log(response.data.data);
this.to = {
id: response.data.data.id,
@@ -283,7 +283,7 @@ export default {
this.tempRates = {};
this.rates = [];
this.loading = true;
- axios.get("./api/v2/exchange-rates/rates/" + this.from_code + '/' + this.to_code + '?page=' + page).then((response) => {
+ axios.get("./api/v1/exchange-rates/rates/" + this.from_code + '/' + this.to_code + '?page=' + page).then((response) => {
for (let i in response.data.data) {
if (response.data.data.hasOwnProperty(i)) {
let current = response.data.data[i];
diff --git a/routes/api.php b/routes/api.php
index 6f296533d6..01ffe8ca3f 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -22,12 +22,6 @@
declare(strict_types=1);
-use FireflyIII\Api\V2\Controllers\JsonApi\AccountController;
-use LaravelJsonApi\Laravel\Facades\JsonApiRoute;
-use LaravelJsonApi\Laravel\Http\Controllers\JsonApiController;
-use LaravelJsonApi\Laravel\Routing\Relationships;
-use LaravelJsonApi\Laravel\Routing\ResourceRegistrar;
-
/*
*
* ____ ____ ___ .______ ______ __ __ .___________. _______ _______.
@@ -105,27 +99,6 @@ Route::group(
}
);
-// exchange rates
-Route::group(
- [
- 'namespace' => 'FireflyIII\Api\V2\Controllers\Model\ExchangeRate',
- 'prefix' => 'v2/exchange-rates',
- 'as' => 'api.v2.exchange-rates.',
- ],
- static function (): void {
- Route::get('', ['uses' => 'IndexController@index', 'as' => 'index']);
- Route::get('rates/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'ShowController@show', 'as' => 'show']);
- Route::delete('rates/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'DestroyController@destroy', 'as' => 'destroy']);
- Route::put('{userGroupExchangeRate}', ['uses' => 'UpdateController@update', 'as' => 'update']);
- Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']);
- //
- // Route::put('{userGroup}', ['uses' => 'UpdateController@update', 'as' => 'update']);
- // Route::post('{userGroup}/use', ['uses' => 'UpdateController@useUserGroup', 'as' => 'use']);
- // Route::put('{userGroup}/update-membership', ['uses' => 'UpdateController@updateMembership', 'as' => 'updateMembership']);
- // Route::delete('{userGroup}', ['uses' => 'DestroyController@destroy', 'as' => 'destroy']);
- }
-);
-
// V2 API route for Summary boxes
// BASIC
@@ -336,6 +309,23 @@ Route::group(
}
);
+// exchange rates
+Route::group(
+ [
+ 'namespace' => 'FireflyIII\Api\V1\Controllers\Model\CurrencyExchangeRate',
+ 'prefix' => 'v1/exchange-rates',
+ 'as' => 'api.v1.exchange-rates.',
+ ],
+ static function (): void {
+ Route::get('', ['uses' => 'IndexController@index', 'as' => 'index']);
+ Route::get('rates/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'ShowController@show', 'as' => 'show']);
+ Route::get('{userGroupExchangeRate}', ['uses' => 'ShowController@showSingle', 'as' => 'show.single']);
+ Route::delete('rates/{fromCurrencyCode}/{toCurrencyCode}', ['uses' => 'DestroyController@destroy', 'as' => 'destroy']);
+ Route::put('{userGroupExchangeRate}', ['uses' => 'UpdateController@update', 'as' => 'update']);
+ Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']);
+ }
+);
+
// CHART ROUTES.
// Chart accounts
Route::group(
@@ -804,6 +794,7 @@ Route::group(
Route::get('', ['uses' => 'ShowController@index', 'as' => 'index']);
Route::post('', ['uses' => 'StoreController@store', 'as' => 'store']);
Route::get('default', ['uses' => 'ShowController@showDefault', 'as' => 'show.default']);
+ Route::get('native', ['uses' => 'ShowController@showDefault', 'as' => 'show.native']);
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']);