From 96ecc5335a141890d5e2c0034d2e9731827ea31a Mon Sep 17 00:00:00 2001 From: Gregor Vostrak Date: Tue, 9 Jun 2026 12:37:20 +0200 Subject: [PATCH] add currency to organization update endpoint --- .../Api/V1/OrganizationController.php | 3 ++ .../OrganizationUpdateRequest.php | 12 ++++- e2e/organization.spec.ts | 27 ++++++++++ .../packages/api/src/openapi.json.client.ts | 1 + .../Api/V1/OrganizationEndpointTest.php | 52 +++++++++++++++++++ 5 files changed, 94 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/V1/OrganizationController.php b/app/Http/Controllers/Api/V1/OrganizationController.php index acf58e49..2d8e327d 100644 --- a/app/Http/Controllers/Api/V1/OrganizationController.php +++ b/app/Http/Controllers/Api/V1/OrganizationController.php @@ -50,6 +50,9 @@ class OrganizationController extends Controller if ($request->getName() !== null) { $organization->name = $request->getName(); } + if ($request->getCurrency() !== null) { + $organization->currency = $request->getCurrency(); + } if ($request->getEmployeesCanSeeBillableRates() !== null) { $organization->employees_can_see_billable_rates = $request->getEmployeesCanSeeBillableRates(); } diff --git a/app/Http/Requests/V1/Organization/OrganizationUpdateRequest.php b/app/Http/Requests/V1/Organization/OrganizationUpdateRequest.php index 4def93fe..f9ff1203 100644 --- a/app/Http/Requests/V1/Organization/OrganizationUpdateRequest.php +++ b/app/Http/Requests/V1/Organization/OrganizationUpdateRequest.php @@ -11,6 +11,7 @@ use App\Enums\NumberFormat; use App\Enums\TimeFormat; use App\Http\Requests\V1\BaseFormRequest; use App\Models\Organization; +use App\Rules\CurrencyRule; use Illuminate\Validation\Rule; /** @@ -21,7 +22,7 @@ class OrganizationUpdateRequest extends BaseFormRequest /** * Get the validation rules that apply to the request. * - * @return array> + * @return array> */ public function rules(): array { @@ -30,6 +31,10 @@ class OrganizationUpdateRequest extends BaseFormRequest 'string', 'max:255', ], + 'currency' => [ + 'string', + new CurrencyRule, + ], 'billable_rate' => array_merge( [ 'nullable', @@ -68,6 +73,11 @@ class OrganizationUpdateRequest extends BaseFormRequest return $this->has('name') ? (string) $this->input('name') : null; } + public function getCurrency(): ?string + { + return $this->has('currency') ? (string) $this->input('currency') : null; + } + public function getNumberFormat(): ?NumberFormat { return $this->has('number_format') ? NumberFormat::from($this->input('number_format')) : null; diff --git a/e2e/organization.spec.ts b/e2e/organization.spec.ts index 7ac9cffe..e8cd5029 100644 --- a/e2e/organization.spec.ts +++ b/e2e/organization.spec.ts @@ -55,6 +55,33 @@ test('test that organization name can be updated', async ({ page }) => { ); }); +test('test that organization currency can be updated', async ({ page }) => { + await goToOrganizationSettings(page); + await page.getByLabel('Currency', { exact: true }).selectOption('USD'); + await Promise.all([ + page.waitForRequest( + (request) => + request.url().includes('/api/v1/organizations/') && + request.method() === 'PUT' && + request.postDataJSON().currency === 'USD' + ), + page.waitForResponse( + async (response) => + response.url().includes('/api/v1/organizations/') && + response.request().method() === 'PUT' && + response.status() === 200 && + (await response.json()).data.currency === 'USD' + ), + page + .locator('form') + .filter({ hasText: 'Organization Name' }) + .getByRole('button', { name: 'Save' }) + .click(), + ]); + await page.reload(); + await expect(page.getByLabel('Currency', { exact: true })).toHaveValue('USD'); +}); + test('test that organization billable rate can be updated with all existing time entries', async ({ page, }) => { diff --git a/resources/js/packages/api/src/openapi.json.client.ts b/resources/js/packages/api/src/openapi.json.client.ts index 564e516a..1313a23f 100644 --- a/resources/js/packages/api/src/openapi.json.client.ts +++ b/resources/js/packages/api/src/openapi.json.client.ts @@ -330,6 +330,7 @@ const OrganizationResource = z const OrganizationUpdateRequest = z .object({ name: z.string().max(255), + currency: z.string(), billable_rate: z.union([z.number(), z.null()]), employees_can_see_billable_rates: z.boolean(), employees_can_manage_tasks: z.boolean(), diff --git a/tests/Unit/Endpoint/Api/V1/OrganizationEndpointTest.php b/tests/Unit/Endpoint/Api/V1/OrganizationEndpointTest.php index 48bf7a8c..76ec58b7 100644 --- a/tests/Unit/Endpoint/Api/V1/OrganizationEndpointTest.php +++ b/tests/Unit/Endpoint/Api/V1/OrganizationEndpointTest.php @@ -382,6 +382,58 @@ class OrganizationEndpointTest extends ApiEndpointTestAbstract ]); } + public function test_update_endpoint_can_update_the_currency_of_the_organization(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'organizations:update', + ]); + $this->assertBillableRateServiceIsUnused(); + $data->organization->currency = 'EUR'; + $data->organization->save(); + Passport::actingAs($data->user); + + // Act + $response = $this->putJson(route('api.v1.organizations.update', [$data->organization->getKey()]), [ + 'name' => $data->organization->name, + 'currency' => 'USD', + ]); + + // Assert + $response->assertStatus(200); + $response->assertJsonPath('data.currency', 'USD'); + $this->assertDatabaseHas(Organization::class, [ + 'id' => $data->organization->getKey(), + 'currency' => 'USD', + ]); + } + + public function test_update_endpoint_fails_if_currency_is_invalid(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'organizations:update', + ]); + $this->assertBillableRateServiceIsUnused(); + $data->organization->currency = 'EUR'; + $data->organization->save(); + Passport::actingAs($data->user); + + // Act + $response = $this->putJson(route('api.v1.organizations.update', [$data->organization->getKey()]), [ + 'name' => $data->organization->name, + 'currency' => 'NOT_A_CURRENCY', + ]); + + // Assert + $response->assertStatus(422); + $response->assertJsonValidationErrors(['currency']); + $this->assertDatabaseHas(Organization::class, [ + 'id' => $data->organization->getKey(), + 'currency' => 'EUR', + ]); + } + public function test_delete_endpoint_if_user_does_not_have_permission(): void { // Arrange