diff --git a/README.md b/README.md index a7a6abb..36af95c 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,20 @@ $client->suppressions->create([ $client->suppressions->delete('alice@example.com', 'marketing'); ``` +Add up to 1,000 at a time with `createBatch`: + +```php +$result = $client->suppressions->createBatch([ + ['email' => 'alice@example.com', 'topic' => 'marketing', 'reason' => 'hard bounce'], + ['email' => 'bob@example.com'], +]); +echo "{$result->imported} added, {$result->already_suppressed} already suppressed\n"; +``` + +Existing entries are counted in `already_suppressed` and left unchanged, so a batch is safe to retry. Invalid addresses come back in `invalid` with their `index` instead of failing the batch. + +Suppressions do not expire; the `expires_at` field is always null, kept for compatibility, and will be removed in a future major version. + `get`, `list` (with `email_contains`, `topic`, `reason`, and `origin` filters), `listForEmail`, and `deleteForEmail` round out the resource. See [Suppressions](https://anypost.com/docs/reference/suppressions) for scoping and the automatic-suppression rules for bounces and complaints. ## Webhooks diff --git a/src/Resources/Suppressions.php b/src/Resources/Suppressions.php index 013a313..05a425e 100644 --- a/src/Resources/Suppressions.php +++ b/src/Resources/Suppressions.php @@ -13,7 +13,7 @@ final class Suppressions extends AbstractResource { /** - * List the team's suppressions, newest-first. Expired rows are filtered out. + * List the team's suppressions, newest-first. * * Filter with `email_contains`, `topic`, `reason`, and `origin`. * @@ -37,6 +37,26 @@ public function create(array $params): Response return $this->requestObject('POST', '/suppressions', ['body' => $params]); } + /** + * Add up to 1,000 manual suppressions in one call. + * + * Each item takes `email` plus optional `topic` (default `*`), `reason` + * (free-form; common aliases such as `bounce` or `opt-out` are mapped, anything + * else records `manual`), and `note`. Addresses already suppressed for the topic + * are counted in `already_suppressed` and left unchanged, so a batch is safe to + * retry. Invalid addresses come back in `invalid` (with their 0-based `index`) + * instead of failing the batch. Throws `validation_error`, adding nothing, if the + * batch would pass the team's suppression limit. + * + * @param list $suppressions + */ + public function createBatch(array $suppressions): Response + { + return $this->requestObject('POST', '/suppressions/batch', [ + 'body' => ['suppressions' => $suppressions], + ]); + } + /** * Retrieve the suppression for an `(email, topic)` pair. * diff --git a/src/Version.php b/src/Version.php index 36bf590..344f88c 100644 --- a/src/Version.php +++ b/src/Version.php @@ -12,5 +12,5 @@ */ final class Version { - public const VERSION = '1.2.0'; + public const VERSION = '1.3.0'; } diff --git a/tests/ResourcesTest.php b/tests/ResourcesTest.php index 2b23d98..e4a1945 100644 --- a/tests/ResourcesTest.php +++ b/tests/ResourcesTest.php @@ -128,6 +128,34 @@ public function test_suppressions_list_for_email_returns_a_list(): void $this->assertSame('news', $rows[1]->topic); } + public function test_suppressions_create_batch_wraps_items_and_parses_the_result(): void + { + $client = $this->client([ + $this->json([ + 'imported' => 1, + 'already_suppressed' => 0, + 'invalid' => [['index' => 1, 'email' => 'not-an-address']], + ]), + ]); + + $result = $client->suppressions->createBatch([ + ['email' => 'a@b.com', 'topic' => 'marketing', 'reason' => 'hard bounce'], + ['email' => 'not-an-address'], + ]); + + $request = $this->lastRequest(); + $this->assertSame('POST', $request->getMethod()); + $this->assertSame('/v1/suppressions/batch', $request->getUri()->getPath()); + $this->assertSame( + '{"suppressions":[{"email":"a@b.com","topic":"marketing","reason":"hard bounce"},{"email":"not-an-address"}]}', + (string) $request->getBody(), + ); + $this->assertSame(1, $result->imported); + $this->assertSame(0, $result->already_suppressed); + $this->assertSame(1, $result->invalid[0]->index); + $this->assertSame('not-an-address', $result->invalid[0]->email); + } + public function test_events_join_tags_as_csv(): void { $client = $this->client([