diff --git a/CHANGELOG.md b/CHANGELOG.md index 566cb2e..208c16e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [3.8.0] - 2025-09-22 +- Add Create Contact event API functionality + ## [3.7.0] - 2025-09-15 - Add Sending Domains API functionality - Add current billing cycle usage diff --git a/README.md b/README.md index a746630..1f08db7 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Currently with this SDK you can: - Send an email (Transactional and Bulk streams) - Send an email with a Template - Send a batch of emails (Transactional and Bulk streams) + - Sending domain management CRUD - Email Sandbox - Send an email - Send an email with a template @@ -34,9 +35,11 @@ Currently with this SDK you can: - Contacts CRUD - Lists CRUD - Import + - Events - General - Templates CRUD - Suppressions management (find and delete) + - Billing info ## Installation diff --git a/examples/general/contacts.php b/examples/general/contacts.php index 9529fdd..aeacb03 100644 --- a/examples/general/contacts.php +++ b/examples/general/contacts.php @@ -2,6 +2,7 @@ use Mailtrap\Config; use Mailtrap\DTO\Request\Contact\CreateContact; +use Mailtrap\DTO\Request\Contact\CreateContactEvent; use Mailtrap\DTO\Request\Contact\ImportContact; use Mailtrap\DTO\Request\Contact\UpdateContact; use Mailtrap\Helper\ResponseHelper; @@ -320,3 +321,30 @@ } catch (Exception $e) { echo 'Caught exception: ', $e->getMessage(), PHP_EOL; } + + +/** + * Create a new Contact Event + * + * POST https://mailtrap.io/api/accounts/{account_id}/contacts/{contact_identifier}/events + */ +try { + // Create event using contact email + $response = $contacts->createContactEvent( + 'john.smith@example.com', // Contact identifier (email or UUID) + CreateContactEvent::init( + 'UserLogin', + [ + 'user_id' => 101, + 'user_name' => 'John Smith', + 'is_active' => true, + 'last_seen' => null + ] + ) + ); + + // print the response body (array) + var_dump(ResponseHelper::toArray($response)); +} catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), PHP_EOL; +} diff --git a/src/Api/General/Contact.php b/src/Api/General/Contact.php index 2d1b338..8d276a9 100644 --- a/src/Api/General/Contact.php +++ b/src/Api/General/Contact.php @@ -7,6 +7,7 @@ use Mailtrap\Api\AbstractApi; use Mailtrap\ConfigInterface; use Mailtrap\DTO\Request\Contact\CreateContact; +use Mailtrap\DTO\Request\Contact\CreateContactEvent; use Mailtrap\DTO\Request\Contact\ImportContact; use Mailtrap\DTO\Request\Contact\UpdateContact; use Mailtrap\Exception\InvalidArgumentException; @@ -290,6 +291,23 @@ public function getContactImport(int $importId): ResponseInterface ); } + /** + * Create a new Contact Event. + * + * @param string $contactIdentifier Contact ID (UUID) or email + * @param CreateContactEvent $event + * @return ResponseInterface + */ + public function createContactEvent(string $contactIdentifier, CreateContactEvent $event): ResponseInterface + { + return $this->handleResponse( + $this->httpPost( + path: $this->getBasePath() . '/' . urlencode($contactIdentifier) . '/events', + body: $event->toArray() + ) + ); + } + public function getAccountId(): int { return $this->accountId; diff --git a/src/DTO/Request/Contact/CreateContactEvent.php b/src/DTO/Request/Contact/CreateContactEvent.php new file mode 100644 index 0000000..9c44256 --- /dev/null +++ b/src/DTO/Request/Contact/CreateContactEvent.php @@ -0,0 +1,42 @@ +name; + } + + public function getParams(): array + { + return $this->params; + } + + public function toArray(): array + { + return [ + 'name' => $this->getName(), + 'params' => $this->getParams(), + ]; + } +} diff --git a/tests/Api/General/ContactTest.php b/tests/Api/General/ContactTest.php index 5d0dc6c..86b7347 100644 --- a/tests/Api/General/ContactTest.php +++ b/tests/Api/General/ContactTest.php @@ -5,6 +5,7 @@ use Mailtrap\Api\AbstractApi; use Mailtrap\Api\General\Contact; use Mailtrap\DTO\Request\Contact\CreateContact; +use Mailtrap\DTO\Request\Contact\CreateContactEvent; use Mailtrap\DTO\Request\Contact\UpdateContact; use Mailtrap\DTO\Request\Contact\ImportContact; use Mailtrap\Exception\HttpClientException; @@ -653,6 +654,277 @@ public function testImportContactsThrowsExceptionForInvalidInput(): void $this->contact->importContacts($contacts); } + public function testCreateContactEvent(): void + { + $contactIdentifier = 'john.smith@example.com'; + $eventData = new CreateContactEvent( + 'UserLogin', + [ + 'user_id' => 101, + 'user_name' => 'John Smith', + 'is_active' => true, + 'last_seen' => null, + ] + ); + + $expectedResponse = [ + 'contact_id' => '018dd5e3-f6d2-7c00-8f9b-e5c3f2d8a132', + 'contact_email' => 'john.smith@example.com', + 'name' => 'UserLogin', + 'params' => [ + 'user_id' => 101, + 'user_name' => 'John Smith', + 'is_active' => true, + 'last_seen' => null, + ], + ]; + + $this->contact->expects($this->once()) + ->method('httpPost') + ->with( + AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/contacts/' . urlencode($contactIdentifier) . '/events', + [], + $eventData->toArray() + ) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($expectedResponse))); + + $response = $this->contact->createContactEvent($contactIdentifier, $eventData); + $responseData = ResponseHelper::toArray($response); + + $this->assertInstanceOf(Response::class, $response); + $this->assertArrayHasKey('contact_id', $responseData); + $this->assertArrayHasKey('name', $responseData); + $this->assertEquals('UserLogin', $responseData['name']); + $this->assertEquals(101, $responseData['params']['user_id']); + } + + public function testCreateContactEventWithContactId(): void + { + $contactId = '018dd5e3-f6d2-7c00-8f9b-e5c3f2d8a132'; + $eventData = new CreateContactEvent( + 'PurchaseCompleted', + [ + 'order_id' => 'ORD-12345', + 'amount' => 99.99, + 'currency' => 'USD', + ] + ); + + $expectedResponse = [ + 'contact_id' => $contactId, + 'contact_email' => 'john.smith@example.com', + 'name' => 'PurchaseCompleted', + 'params' => [ + 'order_id' => 'ORD-12345', + 'amount' => 99.99, + 'currency' => 'USD', + ], + ]; + + $this->contact->expects($this->once()) + ->method('httpPost') + ->with( + AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/contacts/' . urlencode($contactId) . '/events', + [], + $eventData->toArray() + ) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($expectedResponse))); + + $response = $this->contact->createContactEvent($contactId, $eventData); + $responseData = ResponseHelper::toArray($response); + + $this->assertInstanceOf(Response::class, $response); + $this->assertArrayHasKey('contact_id', $responseData); + $this->assertEquals('PurchaseCompleted', $responseData['name']); + $this->assertEquals('ORD-12345', $responseData['params']['order_id']); + } + + public function testCreateContactEventWithEmptyParams(): void + { + $contactIdentifier = 'test@example.com'; + $eventData = new CreateContactEvent('SimpleEvent'); + + $expectedResponse = [ + 'contact_id' => '018dd5e3-f6d2-7c00-8f9b-e5c3f2d8a132', + 'contact_email' => 'test@example.com', + 'name' => 'SimpleEvent', + 'params' => [], + ]; + + $this->contact->expects($this->once()) + ->method('httpPost') + ->with( + AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/contacts/' . urlencode($contactIdentifier) . '/events', + [], + $eventData->toArray() + ) + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($expectedResponse))); + + $response = $this->contact->createContactEvent($contactIdentifier, $eventData); + $responseData = ResponseHelper::toArray($response); + + $this->assertInstanceOf(Response::class, $response); + $this->assertEquals('SimpleEvent', $responseData['name']); + $this->assertEmpty($responseData['params']); + } + + public function testCreateContactEventNotFound(): void + { + $contactIdentifier = 'nonexistent@example.com'; + $eventData = new CreateContactEvent('UserLogin', ['user_id' => 101]); + + $this->contact->expects($this->once()) + ->method('httpPost') + ->with( + AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/contacts/' . urlencode($contactIdentifier) . '/events', + [], + $eventData->toArray() + ) + ->willReturn( + new Response(404, ['Content-Type' => 'application/json'], json_encode(['error' => 'Contact not found'])) + ); + + $this->expectException(HttpClientException::class); + $this->expectExceptionMessage('Errors: Contact not found.'); + + $this->contact->createContactEvent($contactIdentifier, $eventData); + } + + public function testCreateContactEventValidationError(): void + { + $contactIdentifier = 'test@example.com'; + $eventData = new CreateContactEvent('', []); // Empty event name + + $this->contact->expects($this->once()) + ->method('httpPost') + ->with( + AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/contacts/' . urlencode($contactIdentifier) . '/events', + [], + $eventData->toArray() + ) + ->willReturn( + new Response(422, ['Content-Type' => 'application/json'], json_encode([ + 'errors' => [ + 'name' => ['The name field is required.'] + ] + ])) + ); + + $this->expectException(HttpClientException::class); + $this->expectExceptionMessage('Errors: name -> The name field is required.'); + + $this->contact->createContactEvent($contactIdentifier, $eventData); + } + + public function testCreateContactEventUnauthorized(): void + { + $contactIdentifier = 'test@example.com'; + $eventData = new CreateContactEvent('UserLogin', ['user_id' => 101]); + + $this->contact->expects($this->once()) + ->method('httpPost') + ->with( + AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/contacts/' . urlencode($contactIdentifier) . '/events', + [], + $eventData->toArray() + ) + ->willReturn( + new Response(401, ['Content-Type' => 'application/json'], json_encode([ + 'error' => 'Incorrect API token' + ])) + ); + + $this->expectException(HttpClientException::class); + $this->expectExceptionMessage('Errors: Incorrect API token.'); + + $this->contact->createContactEvent($contactIdentifier, $eventData); + } + + public function testCreateContactEventForbidden(): void + { + $contactIdentifier = 'test@example.com'; + $eventData = new CreateContactEvent('UserLogin', ['user_id' => 101]); + + $this->contact->expects($this->once()) + ->method('httpPost') + ->with( + AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/contacts/' . urlencode($contactIdentifier) . '/events', + [], + $eventData->toArray() + ) + ->willReturn( + new Response(403, ['Content-Type' => 'application/json'], json_encode([ + 'errors' => 'Access forbidden' + ])) + ); + + $this->expectException(HttpClientException::class); + $this->expectExceptionMessage('Errors: Access forbidden.'); + + $this->contact->createContactEvent($contactIdentifier, $eventData); + } + + public function testCreateContactEventComplexValidationErrors(): void + { + $contactIdentifier = 'test@example.com'; + $eventData = new CreateContactEvent( + 'VeryLongEventNameThatExceedsTheMaximumAllowedLength', + ['invalid_param_structure' => 'not_a_hash'] + ); + + $this->contact->expects($this->once()) + ->method('httpPost') + ->with( + AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/contacts/' . urlencode($contactIdentifier) . '/events', + [], + $eventData->toArray() + ) + ->willReturn( + new Response(422, ['Content-Type' => 'application/json'], json_encode([ + 'errors' => [ + 'name' => [ + 'must be a string', + 'is too long' + ], + 'params' => [ + 'must be a hash', + "key 'foo' is too long", + "value for 'bar' is too long" + ] + ] + ])) + ); + + $this->expectException(HttpClientException::class); + $this->expectExceptionMessage('Errors: name -> must be a string. is too long. params -> must be a hash. key \'foo\' is too long. value for \'bar\' is too long.'); + + $this->contact->createContactEvent($contactIdentifier, $eventData); + } + + public function testCreateContactEventRateLimitExceeded(): void + { + $contactIdentifier = 'test@example.com'; + $eventData = new CreateContactEvent('UserLogin', ['user_id' => 101]); + + $this->contact->expects($this->once()) + ->method('httpPost') + ->with( + AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/contacts/' . urlencode($contactIdentifier) . '/events', + [], + $eventData->toArray() + ) + ->willReturn( + new Response(429, ['Content-Type' => 'application/json'], json_encode([ + 'errors' => 'Rate limit exceeded' + ])) + ); + + $this->expectException(HttpClientException::class); + $this->expectExceptionMessage('Errors: Rate limit exceeded.'); + + $this->contact->createContactEvent($contactIdentifier, $eventData); + } + private function getExpectedContactFields(): array { return [