From 0739e3ea35f2d5ff643c57b7d045fe45a719a949 Mon Sep 17 00:00:00 2001 From: javorosas Date: Mon, 30 Mar 2026 20:21:51 +0200 Subject: [PATCH 1/7] Add PHP organization team management methods --- src/Resources/Organizations.php | 323 ++++++++++++++++++ .../OrganizationsTeamManagementTest.php | 243 +++++++++++++ 2 files changed, 566 insertions(+) create mode 100644 tests/Resources/OrganizationsTeamManagementTest.php diff --git a/src/Resources/Organizations.php b/src/Resources/Organizations.php index 0d520e7..a981b7a 100644 --- a/src/Resources/Organizations.php +++ b/src/Resources/Organizations.php @@ -452,4 +452,327 @@ public function updateSelfInvoiceSettings($id, $params): mixed throw $e; } } + + /** + * List users with access to an organization. + * + * @param string $organizationId Organization ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function listTeamAccess($organizationId): mixed + { + try { + return json_decode($this->executeGetRequest($this->getRequestUrl($organizationId) . "/team")); + } catch (FacturapiException $e) { + throw $e; + } + } + + /** + * Retrieve one user access entry within an organization. + * + * @param string $organizationId Organization ID. + * @param string $accessId Access ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function retrieveTeamAccess($organizationId, $accessId): mixed + { + try { + return json_decode($this->executeGetRequest($this->getRequestUrl($organizationId) . "/team/" . $accessId)); + } catch (FacturapiException $e) { + throw $e; + } + } + + /** + * Reassign a role to a user access entry. + * + * @param string $organizationId Organization ID. + * @param string $accessId Access ID. + * @param string $role Role ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function updateTeamAccessRole($organizationId, $accessId, $role): mixed + { + try { + return json_decode( + $this->executeJsonPutRequest( + $this->getRequestUrl($organizationId) . "/team/" . $accessId . "/role", + ["role" => $role] + ) + ); + } catch (FacturapiException $e) { + throw $e; + } + } + + /** + * Remove a user access entry from an organization. + * + * @param string $organizationId Organization ID. + * @param string $accessId Access ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function removeTeamAccess($organizationId, $accessId): mixed + { + try { + return json_decode( + $this->executeDeleteRequest( + $this->getRequestUrl($organizationId) . "/team/" . $accessId, + null + ) + ); + } catch (FacturapiException $e) { + throw $e; + } + } + + /** + * List sent team invites for an organization. + * + * @param string $organizationId Organization ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function listSentTeamInvites($organizationId): mixed + { + try { + return json_decode($this->executeGetRequest($this->getRequestUrl($organizationId) . "/team/invites")); + } catch (FacturapiException $e) { + throw $e; + } + } + + /** + * Create or update a team invite for an organization. + * + * @param string $organizationId Organization ID. + * @param array $params Invite payload. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function inviteUserToTeam($organizationId, $params): mixed + { + try { + return json_decode( + $this->executeJsonPostRequest( + $this->getRequestUrl($organizationId) . "/team/invites", + $params + ) + ); + } catch (FacturapiException $e) { + throw $e; + } + } + + /** + * Cancel a pending team invite. + * + * @param string $organizationId Organization ID. + * @param string $inviteKey Invite key. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function cancelTeamInvite($organizationId, $inviteKey): mixed + { + try { + return json_decode( + $this->executeDeleteRequest( + $this->getRequestUrl($organizationId) . "/team/invites/" . $inviteKey, + null + ) + ); + } catch (FacturapiException $e) { + throw $e; + } + } + + /** + * List pending invites received by the authenticated user. + * + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function listReceivedTeamInvites(): mixed + { + try { + return json_decode($this->executeGetRequest($this->getRequestUrl("invites/pending"))); + } catch (FacturapiException $e) { + throw $e; + } + } + + /** + * Respond to a team invite. + * + * @param string $inviteKey Invite key. + * @param array $params Response payload. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function respondTeamInvite($inviteKey, $params): mixed + { + try { + return json_decode( + $this->executeJsonPostRequest( + $this->getRequestUrl("invites/" . $inviteKey . "/response"), + $params + ) + ); + } catch (FacturapiException $e) { + throw $e; + } + } + + /** + * List organization team roles. + * + * @param string $organizationId Organization ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function listTeamRoles($organizationId): mixed + { + try { + return json_decode($this->executeGetRequest($this->getRequestUrl($organizationId) . "/team/roles")); + } catch (FacturapiException $e) { + throw $e; + } + } + + /** + * List team role templates. + * + * @param string $organizationId Organization ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function listTeamRoleTemplates($organizationId): mixed + { + try { + return json_decode($this->executeGetRequest($this->getRequestUrl($organizationId) . "/team/roles/templates")); + } catch (FacturapiException $e) { + throw $e; + } + } + + /** + * List team role operations. + * + * @param string $organizationId Organization ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function listTeamRoleOperations($organizationId): mixed + { + try { + return json_decode($this->executeGetRequest($this->getRequestUrl($organizationId) . "/team/roles/operations")); + } catch (FacturapiException $e) { + throw $e; + } + } + + /** + * Retrieve a team role by ID. + * + * @param string $organizationId Organization ID. + * @param string $roleId Role ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function retrieveTeamRole($organizationId, $roleId): mixed + { + try { + return json_decode($this->executeGetRequest($this->getRequestUrl($organizationId) . "/team/roles/" . $roleId)); + } catch (FacturapiException $e) { + throw $e; + } + } + + /** + * Create a team role. + * + * @param string $organizationId Organization ID. + * @param array $params Role payload. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function createTeamRole($organizationId, $params): mixed + { + try { + return json_decode( + $this->executeJsonPostRequest( + $this->getRequestUrl($organizationId) . "/team/roles", + $params + ) + ); + } catch (FacturapiException $e) { + throw $e; + } + } + + /** + * Update a team role. + * + * @param string $organizationId Organization ID. + * @param string $roleId Role ID. + * @param array $params Role payload. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function updateTeamRole($organizationId, $roleId, $params): mixed + { + try { + return json_decode( + $this->executeJsonPutRequest( + $this->getRequestUrl($organizationId) . "/team/roles/" . $roleId, + $params + ) + ); + } catch (FacturapiException $e) { + throw $e; + } + } + + /** + * Delete a team role. + * + * @param string $organizationId Organization ID. + * @param string $roleId Role ID. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException + */ + public function deleteTeamRole($organizationId, $roleId): mixed + { + try { + return json_decode( + $this->executeDeleteRequest( + $this->getRequestUrl($organizationId) . "/team/roles/" . $roleId, + null + ) + ); + } catch (FacturapiException $e) { + throw $e; + } + } } diff --git a/tests/Resources/OrganizationsTeamManagementTest.php b/tests/Resources/OrganizationsTeamManagementTest.php new file mode 100644 index 0000000..3424d13 --- /dev/null +++ b/tests/Resources/OrganizationsTeamManagementTest.php @@ -0,0 +1,243 @@ + $httpClient]); + + $result = $organizations->listTeamAccess('org_123'); + + self::assertIsArray($result); + $request = $httpClient->requests()[0]; + self::assertSame('GET', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team', (string) $request->getUri()); + } + + public function testRetrieveTeamAccessUsesExpectedEndpoint(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '{"id":"acc_123"}')); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + + $result = $organizations->retrieveTeamAccess('org_123', 'acc_123'); + + self::assertSame('acc_123', $result->id); + $request = $httpClient->requests()[0]; + self::assertSame('GET', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/acc_123', (string) $request->getUri()); + } + + public function testInviteUserToTeamUsesExpectedEndpointAndJsonBody(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '{"id":"inv_123"}')); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + + $result = $organizations->inviteUserToTeam('org_123', [ + 'email' => 'alex@example.com', + 'role' => 'role_123', + ]); + + self::assertSame('inv_123', $result->id); + $request = $httpClient->requests()[0]; + self::assertSame('POST', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/invites', (string) $request->getUri()); + self::assertSame('application/json', $request->getHeaderLine('Content-Type')); + self::assertStringContainsString('"email":"alex@example.com"', (string) $request->getBody()); + self::assertStringContainsString('"role":"role_123"', (string) $request->getBody()); + } + + public function testListSentTeamInvitesUsesExpectedEndpoint(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '[]')); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + + $result = $organizations->listSentTeamInvites('org_123'); + + self::assertIsArray($result); + $request = $httpClient->requests()[0]; + self::assertSame('GET', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/invites', (string) $request->getUri()); + } + + public function testCancelTeamInviteUsesExpectedEndpoint(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '{"ok":true}')); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + + $result = $organizations->cancelTeamInvite('org_123', 'inv_123'); + + self::assertTrue($result->ok); + $request = $httpClient->requests()[0]; + self::assertSame('DELETE', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/invites/inv_123', (string) $request->getUri()); + } + + public function testListReceivedTeamInvitesUsesExpectedEndpoint(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '[]')); + $organizations = new Organizations('sk_user_abc123', ['httpClient' => $httpClient]); + + $result = $organizations->listReceivedTeamInvites(); + + self::assertIsArray($result); + $request = $httpClient->requests()[0]; + self::assertSame('GET', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/organizations/invites/pending', (string) $request->getUri()); + } + + public function testRespondTeamInviteUsesExpectedEndpointAndJsonBody(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '{"ok":true}')); + $organizations = new Organizations('sk_user_abc123', ['httpClient' => $httpClient]); + + $result = $organizations->respondTeamInvite('inv_123', ['accept' => true]); + + self::assertTrue($result->ok); + $request = $httpClient->requests()[0]; + self::assertSame('POST', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/organizations/invites/inv_123/response', (string) $request->getUri()); + self::assertSame('application/json', $request->getHeaderLine('Content-Type')); + self::assertStringContainsString('"accept":true', (string) $request->getBody()); + } + + public function testListTeamRolesUsesExpectedEndpoint(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '[]')); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + + $result = $organizations->listTeamRoles('org_123'); + + self::assertIsArray($result); + $request = $httpClient->requests()[0]; + self::assertSame('GET', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/roles', (string) $request->getUri()); + } + + public function testListTeamRoleTemplatesUsesExpectedEndpoint(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '[]')); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + + $result = $organizations->listTeamRoleTemplates('org_123'); + + self::assertIsArray($result); + $request = $httpClient->requests()[0]; + self::assertSame('GET', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/roles/templates', (string) $request->getUri()); + } + + public function testListTeamRoleOperationsUsesExpectedEndpoint(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '[]')); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + + $result = $organizations->listTeamRoleOperations('org_123'); + + self::assertIsArray($result); + $request = $httpClient->requests()[0]; + self::assertSame('GET', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/roles/operations', (string) $request->getUri()); + } + + public function testRetrieveTeamRoleUsesExpectedEndpoint(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '{"id":"role_123"}')); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + + $result = $organizations->retrieveTeamRole('org_123', 'role_123'); + + self::assertSame('role_123', $result->id); + $request = $httpClient->requests()[0]; + self::assertSame('GET', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/roles/role_123', (string) $request->getUri()); + } + + public function testCreateTeamRoleUsesExpectedEndpointAndJsonBody(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '{"id":"role_123","name":"Billing analyst"}')); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + + $result = $organizations->createTeamRole('org_123', [ + 'name' => 'Billing analyst', + 'operations' => ['read:invoices'], + ]); + + self::assertSame('role_123', $result->id); + $request = $httpClient->requests()[0]; + self::assertSame('POST', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/roles', (string) $request->getUri()); + self::assertSame('application/json', $request->getHeaderLine('Content-Type')); + self::assertStringContainsString('"name":"Billing analyst"', (string) $request->getBody()); + self::assertStringContainsString('"operations":["read:invoices"]', (string) $request->getBody()); + } + + public function testUpdateTeamAccessRoleUsesExpectedEndpointAndJsonBody(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '{"id":"acc_123","role":"role_123"}')); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + + $result = $organizations->updateTeamAccessRole('org_123', 'acc_123', 'role_123'); + + self::assertSame('acc_123', $result->id); + self::assertSame('role_123', $result->role); + $request = $httpClient->requests()[0]; + self::assertSame('PUT', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/acc_123/role', (string) $request->getUri()); + self::assertSame('application/json', $request->getHeaderLine('Content-Type')); + self::assertStringContainsString('"role":"role_123"', (string) $request->getBody()); + } + + public function testRemoveTeamAccessUsesExpectedEndpoint(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '{"ok":true}')); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + + $result = $organizations->removeTeamAccess('org_123', 'acc_123'); + + self::assertTrue($result->ok); + $request = $httpClient->requests()[0]; + self::assertSame('DELETE', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/acc_123', (string) $request->getUri()); + } + + public function testUpdateTeamRoleUsesExpectedEndpointAndJsonBody(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '{"id":"role_123","name":"Senior billing analyst"}')); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + + $result = $organizations->updateTeamRole('org_123', 'role_123', [ + 'name' => 'Senior billing analyst', + ]); + + self::assertSame('role_123', $result->id); + self::assertSame('Senior billing analyst', $result->name); + $request = $httpClient->requests()[0]; + self::assertSame('PUT', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/roles/role_123', (string) $request->getUri()); + self::assertSame('application/json', $request->getHeaderLine('Content-Type')); + self::assertStringContainsString('"name":"Senior billing analyst"', (string) $request->getBody()); + } + + public function testDeleteTeamRoleUsesExpectedEndpoint(): void + { + $httpClient = new FakeHttpClient(new Response(200, [], '{"ok":true}')); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + + $result = $organizations->deleteTeamRole('org_123', 'role_123'); + + self::assertTrue($result->ok); + $request = $httpClient->requests()[0]; + self::assertSame('DELETE', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/organizations/org_123/team/roles/role_123', (string) $request->getUri()); + } +} From 27d3a8f447bdbb2f77fb7668f169bac0fce75fbf Mon Sep 17 00:00:00 2001 From: javorosas Date: Tue, 31 Mar 2026 11:13:34 +0200 Subject: [PATCH 2/7] test/docs: address copilot feedback on cleanup and domain-check docs --- src/Resources/Organizations.php | 6 +- .../Resources/OrganizationsMultipartTest.php | 82 ++++++++++--------- 2 files changed, 48 insertions(+), 40 deletions(-) diff --git a/src/Resources/Organizations.php b/src/Resources/Organizations.php index a981b7a..a8086d2 100644 --- a/src/Resources/Organizations.php +++ b/src/Resources/Organizations.php @@ -136,7 +136,11 @@ public function updateDomain($id, $params): mixed /** * Check domain availability. * - * @param array $params Domain check parameters. + * @param array|string $idOrParams Domain check parameters when called as + * `checkDomainIsAvailable($params)`, or organization id when called + * with the deprecated signature `checkDomainIsAvailable($id, $params)`. + * @param array|null $params Domain check parameters for the deprecated + * two-argument signature. * @return mixed JSON-decoded response. * * @throws FacturapiException diff --git a/tests/Resources/OrganizationsMultipartTest.php b/tests/Resources/OrganizationsMultipartTest.php index 5fd3bc4..746433b 100644 --- a/tests/Resources/OrganizationsMultipartTest.php +++ b/tests/Resources/OrganizationsMultipartTest.php @@ -17,24 +17,26 @@ public function testUploadLogoBuildsMultipartRequest(): void $tmpLogo = tempnam(sys_get_temp_dir(), 'logo_'); file_put_contents($tmpLogo, 'LOGO_BYTES'); - $httpClient = new FakeHttpClient(new Response(200, [], '{"ok":true}')); - $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); - - $result = $organizations->uploadLogo('org_123', $tmpLogo); + try { + $httpClient = new FakeHttpClient(new Response(200, [], '{"ok":true}')); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); - self::assertTrue($result->ok); + $result = $organizations->uploadLogo('org_123', $tmpLogo); - $request = $httpClient->requests()[0]; - self::assertSame('PUT', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/organizations/org_123/logo', (string) $request->getUri()); - self::assertStringStartsWith('multipart/form-data; boundary=', $request->getHeaderLine('Content-Type')); + self::assertTrue($result->ok); - $body = (string) $request->getBody(); - self::assertStringContainsString('name="file"', $body); - self::assertStringContainsString('filename="' . basename($tmpLogo) . '"', $body); - self::assertStringContainsString('LOGO_BYTES', $body); + $request = $httpClient->requests()[0]; + self::assertSame('PUT', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/organizations/org_123/logo', (string) $request->getUri()); + self::assertStringStartsWith('multipart/form-data; boundary=', $request->getHeaderLine('Content-Type')); - @unlink($tmpLogo); + $body = (string) $request->getBody(); + self::assertStringContainsString('name="file"', $body); + self::assertStringContainsString('filename="' . basename($tmpLogo) . '"', $body); + self::assertStringContainsString('LOGO_BYTES', $body); + } finally { + @unlink($tmpLogo); + } } public function testUploadCertificateBuildsMultipartRequestWithCerKeyAndPassword(): void @@ -44,31 +46,33 @@ public function testUploadCertificateBuildsMultipartRequestWithCerKeyAndPassword file_put_contents($tmpCer, 'CER_BYTES'); file_put_contents($tmpKey, 'KEY_BYTES'); - $httpClient = new FakeHttpClient(new Response(200, [], '{"ok":true}')); - $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); - - $result = $organizations->uploadCertificate('org_123', [ - 'cerFile' => $tmpCer, - 'keyFile' => $tmpKey, - 'password' => 'secret_password', - ]); - - self::assertTrue($result->ok); - - $request = $httpClient->requests()[0]; - self::assertSame('PUT', $request->getMethod()); - self::assertSame('https://www.facturapi.io/v2/organizations/org_123/certificate', (string) $request->getUri()); - - $body = (string) $request->getBody(); - self::assertStringContainsString('name="cer"', $body); - self::assertStringContainsString('name="key"', $body); - self::assertStringContainsString('name="password"', $body); - self::assertStringContainsString('CER_BYTES', $body); - self::assertStringContainsString('KEY_BYTES', $body); - self::assertStringContainsString('secret_password', $body); - - @unlink($tmpCer); - @unlink($tmpKey); + try { + $httpClient = new FakeHttpClient(new Response(200, [], '{"ok":true}')); + $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); + + $result = $organizations->uploadCertificate('org_123', [ + 'cerFile' => $tmpCer, + 'keyFile' => $tmpKey, + 'password' => 'secret_password', + ]); + + self::assertTrue($result->ok); + + $request = $httpClient->requests()[0]; + self::assertSame('PUT', $request->getMethod()); + self::assertSame('https://www.facturapi.io/v2/organizations/org_123/certificate', (string) $request->getUri()); + + $body = (string) $request->getBody(); + self::assertStringContainsString('name="cer"', $body); + self::assertStringContainsString('name="key"', $body); + self::assertStringContainsString('name="password"', $body); + self::assertStringContainsString('CER_BYTES', $body); + self::assertStringContainsString('KEY_BYTES', $body); + self::assertStringContainsString('secret_password', $body); + } finally { + @unlink($tmpCer); + @unlink($tmpKey); + } } public function testUploadCertificateFailsWhenRequiredFieldsAreMissing(): void From dbed8b9e3e6845a87462c223a4843424eb68d913 Mon Sep 17 00:00:00 2001 From: javorosas Date: Tue, 31 Mar 2026 11:15:03 +0200 Subject: [PATCH 3/7] fix(organizations): keep checkDomainIsAvailable params-only --- src/Resources/Organizations.php | 22 ++++---------- tests/Resources/OrganizationsDomainTest.php | 33 ++++++--------------- 2 files changed, 14 insertions(+), 41 deletions(-) diff --git a/src/Resources/Organizations.php b/src/Resources/Organizations.php index a8086d2..5f5a5bf 100644 --- a/src/Resources/Organizations.php +++ b/src/Resources/Organizations.php @@ -136,33 +136,21 @@ public function updateDomain($id, $params): mixed /** * Check domain availability. * - * @param array|string $idOrParams Domain check parameters when called as - * `checkDomainIsAvailable($params)`, or organization id when called - * with the deprecated signature `checkDomainIsAvailable($id, $params)`. - * @param array|null $params Domain check parameters for the deprecated - * two-argument signature. + * @param array $params Domain check parameters. * @return mixed JSON-decoded response. * * @throws FacturapiException */ - public function checkDomainIsAvailable($idOrParams, $params = null): mixed + public function checkDomainIsAvailable($params): mixed { - $argsCount = func_num_args(); - $query = null; - if ($argsCount === 1 && is_array($idOrParams)) { - $query = $idOrParams; - } elseif ($argsCount >= 2 && is_array($params)) { - // Backward compatibility with historical signature: ($id, $params). - trigger_error('Organizations::checkDomainIsAvailable($id, $params) is deprecated and will be removed in v5. Use checkDomainIsAvailable($params) instead.', E_USER_DEPRECATED); - $query = $params; - } else { - throw new FacturapiException('checkDomainIsAvailable expects either ($params) or ($id, $params).'); + if (!is_array($params)) { + throw new FacturapiException('checkDomainIsAvailable expects $params to be an array.'); } try { return json_decode( $this->executeGetRequest( - $this->getRequestUrl("domain-check", $query) + $this->getRequestUrl("domain-check", $params) ) ); } catch (FacturapiException $e) { diff --git a/tests/Resources/OrganizationsDomainTest.php b/tests/Resources/OrganizationsDomainTest.php index 666691a..f0178f6 100644 --- a/tests/Resources/OrganizationsDomainTest.php +++ b/tests/Resources/OrganizationsDomainTest.php @@ -31,42 +31,27 @@ public function testCheckDomainIsAvailableUsesExpectedEndpoint(): void ); } - public function testCheckDomainIsAvailableAcceptsLegacyTwoArgumentSignature(): void + public function testCheckDomainAvailabilityAliasDelegatesToCanonicalMethod(): void { $httpClient = new FakeHttpClient(new Response(200, [], '{"available":true}')); $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); - $captured = []; - set_error_handler(static function (int $severity, string $message) use (&$captured): bool { - $captured[] = ['severity' => $severity, 'message' => $message]; - return true; - }); - - try { - $result = $organizations->checkDomainIsAvailable('org_ignored', [ - 'name' => 'acme', - 'domain' => 'acme.mx', - ]); - } finally { - restore_error_handler(); - } + $result = $organizations->checkDomainAvailability([ + 'name' => 'acme', + 'domain' => 'acme.mx', + ]); self::assertTrue($result->available); - self::assertNotEmpty($captured); - self::assertSame(E_USER_DEPRECATED, $captured[0]['severity']); - self::assertStringContainsString('checkDomainIsAvailable($params)', $captured[0]['message']); } - public function testCheckDomainAvailabilityAliasDelegatesToCanonicalMethod(): void + public function testCheckDomainIsAvailableThrowsWhenParamsAreNotArray(): void { $httpClient = new FakeHttpClient(new Response(200, [], '{"available":true}')); $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); - $result = $organizations->checkDomainAvailability([ - 'name' => 'acme', - 'domain' => 'acme.mx', - ]); + $this->expectException(\Facturapi\Exceptions\FacturapiException::class); + $this->expectExceptionMessage('checkDomainIsAvailable expects $params to be an array.'); - self::assertTrue($result->available); + $organizations->checkDomainIsAvailable('invalid'); } } From 8464b2e5cc33d9791cf2be6b98d0f00a647f811c Mon Sep 17 00:00:00 2001 From: javorosas Date: Tue, 31 Mar 2026 11:28:11 +0200 Subject: [PATCH 4/7] refactor(organizations): use query naming for domain-check params --- src/Resources/Organizations.php | 14 +++++++------- tests/Resources/OrganizationsDomainTest.php | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Resources/Organizations.php b/src/Resources/Organizations.php index 5f5a5bf..bf5a3d8 100644 --- a/src/Resources/Organizations.php +++ b/src/Resources/Organizations.php @@ -136,21 +136,21 @@ public function updateDomain($id, $params): mixed /** * Check domain availability. * - * @param array $params Domain check parameters. + * @param array $query Domain check query parameters. * @return mixed JSON-decoded response. * * @throws FacturapiException */ - public function checkDomainIsAvailable($params): mixed + public function checkDomainIsAvailable($query): mixed { - if (!is_array($params)) { - throw new FacturapiException('checkDomainIsAvailable expects $params to be an array.'); + if (!is_array($query)) { + throw new FacturapiException('checkDomainIsAvailable expects $query to be an array.'); } try { return json_decode( $this->executeGetRequest( - $this->getRequestUrl("domain-check", $params) + $this->getRequestUrl("domain-check", $query) ) ); } catch (FacturapiException $e) { @@ -161,9 +161,9 @@ public function checkDomainIsAvailable($params): mixed /** * Alias for consistency with API operation naming. */ - public function checkDomainAvailability($params): mixed + public function checkDomainAvailability($query): mixed { - return $this->checkDomainIsAvailable($params); + return $this->checkDomainIsAvailable($query); } /** diff --git a/tests/Resources/OrganizationsDomainTest.php b/tests/Resources/OrganizationsDomainTest.php index f0178f6..2e1f6b3 100644 --- a/tests/Resources/OrganizationsDomainTest.php +++ b/tests/Resources/OrganizationsDomainTest.php @@ -50,7 +50,7 @@ public function testCheckDomainIsAvailableThrowsWhenParamsAreNotArray(): void $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); $this->expectException(\Facturapi\Exceptions\FacturapiException::class); - $this->expectExceptionMessage('checkDomainIsAvailable expects $params to be an array.'); + $this->expectExceptionMessage('checkDomainIsAvailable expects $query to be an array.'); $organizations->checkDomainIsAvailable('invalid'); } From bbeef207bb208d5ee46cbd87812103e1d432b7fe Mon Sep 17 00:00:00 2001 From: javorosas Date: Tue, 31 Mar 2026 11:30:05 +0200 Subject: [PATCH 5/7] chore: ignore phpunit cache directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 15de973..02a0e55 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ composer.phar composer.lock /.idea/ .DS_Store +.phpunit.cache/ From a75ae4b862ddb1629c29486b76aedb2aabcce7b7 Mon Sep 17 00:00:00 2001 From: javorosas Date: Tue, 31 Mar 2026 12:20:07 +0200 Subject: [PATCH 6/7] chore(domain): make checkDomainAvailability canonical and deprecate alias --- VERSION.md | 40 +++++---------------- src/Resources/Organizations.php | 31 +++++++++++++--- tests/Resources/OrganizationsDomainTest.php | 14 +++++--- 3 files changed, 44 insertions(+), 41 deletions(-) diff --git a/VERSION.md b/VERSION.md index b75cb02..61eebed 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1,31 +1,9 @@ -4.0.0 - -## 🚨 Breaking Changes -- Impact: update required only if you use PHP < 8.2 or relied on the positional `apiVersion` constructor argument. -- Bumped minimum supported PHP version to `>=8.2`. -- Simplified SDK constructor to a single optional `config` parameter: - - `apiVersion` (defaults to `v2`) - - `timeout` (seconds) - - `httpClient` (PSR-18 client, advanced) -- Removed support for the positional `apiVersion` constructor argument. - -## Safe Update (No Migration Needed) -- You can update without code changes if: - - Your project already runs on PHP `>=8.2`. - - You instantiate the SDK as `new Facturapi($apiKey)` (single API key argument). - - If you use Composer, keep loading `vendor/autoload.php` as usual. - - If you do not use Composer, load `src/Facturapi.php` directly. - -## Deprecations (Non-Breaking) -- Snake_case aliases remain functional in v4, but are deprecated and will be removed in v5. -- `Facturapi_Exception` remains functional in v4 through a compatibility alias, but is deprecated and will be removed in v5. - -## Improvements -- Added camelCase method names for invoice/receipt/retention actions. -- Kept snake_case aliases for transition compatibility. -- Enabled strict TLS verification by default for HTTP requests. -- `Webhooks::validateSignature()` now verifies locally by default when payload includes `body`/`payload`, `signature`, and `webhookSecret`, with automatic fallback to API validation. -- Added `ComercioExteriorCatalogs` to the main `Facturapi` client. -- Standardized constructor argument names to camelCase. -- Standardized internal protected method names to camelCase. -- Renamed exception class to `FacturapiException` and kept `Facturapi_Exception` as a compatibility alias. +4.1.0 + +## Added +- Organization team/access management methods: + - `listTeamAccess`, `retrieveTeamAccess`, `removeTeamAccess` + - `listSentTeamInvites`, `inviteUserToTeam`, `cancelTeamInvite` + - `listReceivedTeamInvites`, `respondTeamInvite` + - `listTeamRoles`, `listTeamRoleTemplates`, `listTeamRoleOperations` + - `retrieveTeamRole`, `createTeamRole`, `updateTeamRole`, `deleteTeamRole` diff --git a/src/Resources/Organizations.php b/src/Resources/Organizations.php index bf5a3d8..d6c37d8 100644 --- a/src/Resources/Organizations.php +++ b/src/Resources/Organizations.php @@ -141,10 +141,10 @@ public function updateDomain($id, $params): mixed * * @throws FacturapiException */ - public function checkDomainIsAvailable($query): mixed + public function checkDomainAvailability($query): mixed { if (!is_array($query)) { - throw new FacturapiException('checkDomainIsAvailable expects $query to be an array.'); + throw new FacturapiException('checkDomainAvailability expects $query to be an array.'); } try { @@ -159,11 +159,32 @@ public function checkDomainIsAvailable($query): mixed } /** - * Alias for consistency with API operation naming. + * @deprecated Use checkDomainAvailability($query) instead. This alias will be removed in v5. + * + * @param array|string $idOrQuery Domain check query parameters, or legacy organization ID. + * @param array|null $query Legacy query parameters when using historical signature. + * @return mixed JSON-decoded response. + * + * @throws FacturapiException */ - public function checkDomainAvailability($query): mixed + public function checkDomainIsAvailable($idOrQuery, $query = null): mixed { - return $this->checkDomainIsAvailable($query); + trigger_error( + 'Organizations::checkDomainIsAvailable(...) is deprecated and will be removed in v5. Use checkDomainAvailability($query) instead.', + E_USER_DEPRECATED + ); + + $argsCount = func_num_args(); + if ($argsCount === 1 && is_array($idOrQuery)) { + return $this->checkDomainAvailability($idOrQuery); + } + + if ($argsCount >= 2 && is_array($query)) { + // Backward compatibility with historical signature: ($id, $params). + return $this->checkDomainAvailability($query); + } + + throw new FacturapiException('checkDomainIsAvailable expects either ($query) or ($id, $params).'); } /** diff --git a/tests/Resources/OrganizationsDomainTest.php b/tests/Resources/OrganizationsDomainTest.php index 2e1f6b3..239688e 100644 --- a/tests/Resources/OrganizationsDomainTest.php +++ b/tests/Resources/OrganizationsDomainTest.php @@ -11,12 +11,12 @@ final class OrganizationsDomainTest extends TestCase { - public function testCheckDomainIsAvailableUsesExpectedEndpoint(): void + public function testCheckDomainAvailabilityUsesExpectedEndpoint(): void { $httpClient = new FakeHttpClient(new Response(200, [], '{"available":true}')); $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); - $result = $organizations->checkDomainIsAvailable([ + $result = $organizations->checkDomainAvailability([ 'name' => 'acme', 'domain' => 'acme.mx', ]); @@ -31,12 +31,16 @@ public function testCheckDomainIsAvailableUsesExpectedEndpoint(): void ); } - public function testCheckDomainAvailabilityAliasDelegatesToCanonicalMethod(): void + public function testCheckDomainIsAvailableAliasIsDeprecatedAndDelegatesToCanonicalMethod(): void { $httpClient = new FakeHttpClient(new Response(200, [], '{"available":true}')); $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); - $result = $organizations->checkDomainAvailability([ + $this->expectUserDeprecationMessage( + 'Organizations::checkDomainIsAvailable(...) is deprecated and will be removed in v5. Use checkDomainAvailability($query) instead.' + ); + + $result = $organizations->checkDomainIsAvailable([ 'name' => 'acme', 'domain' => 'acme.mx', ]); @@ -50,7 +54,7 @@ public function testCheckDomainIsAvailableThrowsWhenParamsAreNotArray(): void $organizations = new Organizations('sk_test_abc123', ['httpClient' => $httpClient]); $this->expectException(\Facturapi\Exceptions\FacturapiException::class); - $this->expectExceptionMessage('checkDomainIsAvailable expects $query to be an array.'); + $this->expectExceptionMessage('checkDomainIsAvailable expects either ($query) or ($id, $params).'); $organizations->checkDomainIsAvailable('invalid'); } From 9a63d3debeaf66b53bac5e6d72fc83f3eb79d940 Mon Sep 17 00:00:00 2001 From: javorosas Date: Tue, 31 Mar 2026 12:24:57 +0200 Subject: [PATCH 7/7] docs(version): note canonical domain-check method and alias deprecation --- VERSION.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/VERSION.md b/VERSION.md index 61eebed..28e566c 100644 --- a/VERSION.md +++ b/VERSION.md @@ -7,3 +7,7 @@ - `listReceivedTeamInvites`, `respondTeamInvite` - `listTeamRoles`, `listTeamRoleTemplates`, `listTeamRoleOperations` - `retrieveTeamRole`, `createTeamRole`, `updateTeamRole`, `deleteTeamRole` + +## Changed +- `organizations.checkDomainAvailability($query)` is now the canonical method for domain checks. +- `organizations.checkDomainIsAvailable(...)` remains available as a deprecated alias for v4 compatibility.