Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/Broadcasts/Recipient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Resend\Broadcasts;

use Resend\Resource;

/**
* @property string $id Opaque cursor identifying this row, used for pagination.
* @property null|string $contact_id The ID of the contact associated with this recipient, if one exists.
* @property string $email The recipient's email address.
* @property int $count The number of times this recipient triggered the event. Only present when the requested type is `opened` or `clicked`.
* @property string $bounce_type The type of bounce: `permanent`, `transient`, or `undetermined`. Only present when the requested type is `bounced`.
* @property array $clicked_links The links this recipient clicked. Only present when the requested type is `clicked`.
*/
class Recipient extends Resource
{
//
}
17 changes: 17 additions & 0 deletions src/Service/Broadcast.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,23 @@ public function list(array $options = []): \Resend\Collection
return $this->createResource('broadcasts', $result);
}

/**
* Retrieve the recipients of a broadcast for a given event type.
*
* @param array{'type': string, 'email'?: string, 'bounce_type'?: string, 'limit'?: int, 'before'?: string, 'after'?: string} $options
* @return \Resend\Collection<\Resend\Broadcasts\Recipient>
*
* @see https://resend.com/docs/api-reference/broadcasts/list-broadcast-recipients
*/
public function recipients(string $id, array $options): \Resend\Collection
Comment thread
dielduarte marked this conversation as resolved.
{
$payload = Payload::list("broadcasts/{$id}/recipients", $options, ['type', 'email', 'bounce_type']);

$result = $this->transporter->request($payload);

return $this->createResource('broadcast-recipients', $result);
}

/**
* Update a broadcast to send to your audience.
*
Expand Down
2 changes: 2 additions & 0 deletions src/Service/Service.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Resend\Automations\Run as AutomationRun;
use Resend\Broadcast;
use Resend\Broadcasts\ClickedLink as BroadcastClickedLink;
use Resend\Broadcasts\Recipient as BroadcastRecipient;
use Resend\Collection;
use Resend\Contact;
use Resend\ContactProperty;
Expand Down Expand Up @@ -41,6 +42,7 @@ abstract class Service
'automations' => Automation::class,
'broadcasts' => Broadcast::class,
'broadcast-clicked-links' => BroadcastClickedLink::class,
'broadcast-recipients' => BroadcastRecipient::class,
'contact-imports' => ContactImport::class,
'contact-properties' => ContactProperty::class,
'contact-topics' => ContactTopic::class,
Expand Down
4 changes: 2 additions & 2 deletions src/ValueObjects/Transporter/Payload.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ public function __construct(
/**
* Create a new Transporter Payload instance.
*/
public static function list(string $resource, array $options = []): self
public static function list(string $resource, array $options = [], array $extraAllowedParams = []): self
{
$contentType = ContentType::JSON;
$method = Method::GET;

// whitelist keys that are actually valid query params, remove anything else
$allowedParams = ['limit', 'after', 'before', 'status', 'origin'];
$allowedParams = array_merge(['limit', 'after', 'before', 'status', 'origin'], $extraAllowedParams);
$searchParams = array_intersect_key($options, array_flip($allowedParams));

$uri = ResourceUri::list(! empty($searchParams) ? $resource . '?' . http_build_query($searchParams) : $resource);
Expand Down
35 changes: 35 additions & 0 deletions tests/Fixtures/Broadcast.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,38 @@ function broadcasts(): array
],
];
}

function broadcastRecipients(): array
{
return [
'object' => 'list',
'has_more' => true,
'data' => [
[
'id' => 'b2Zmc2V0OjA',
'contact_id' => 'e169aa45-1ecf-4183-9955-b1499d5701d3',
'email' => 'carter@example.com',
'count' => 3,
'clicked_links' => [
['url' => 'https://resend.com/pricing', 'clicks' => 2],
],
],
],
];
}

function broadcastBouncedRecipients(): array
{
return [
'object' => 'list',
'has_more' => false,
'data' => [
[
'id' => 'b2Zmc2V0OjE',
'contact_id' => null,
'email' => 'dana@example.com',
'bounce_type' => 'permanent',
],
],
];
}
59 changes: 59 additions & 0 deletions tests/Service/Broadcast.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<?php

use Resend\Broadcast;
use Resend\Broadcasts\Recipient;
use Resend\Client;
use Resend\Collection;
use Resend\Contracts\Transporter;
use Resend\Exceptions\ErrorException;

it('can get a broadcase resource', function () {
$client = mockClient('GET', 'broadcasts/559ac32e-9ef5-46fb-82a1-b76b840c0f7b', [], [], broadcast());
Expand Down Expand Up @@ -49,6 +53,61 @@
->data->toBeArray();
});

it('can get a list of broadcast recipients', function () {
$client = mockClient('GET', 'broadcasts/559ac32e-9ef5-46fb-82a1-b76b840c0f7b/recipients?type=clicked&limit=20', [], [], broadcastRecipients());

$result = $client->broadcasts->recipients('559ac32e-9ef5-46fb-82a1-b76b840c0f7b', [
'type' => 'clicked',
'limit' => 20,
]);

expect($result)->toBeInstanceOf(Collection::class)
->has_more->toBeTrue();

expect($result->data[0])->toBeInstanceOf(Recipient::class)
->id->toBe('b2Zmc2V0OjA')
->contact_id->toBe('e169aa45-1ecf-4183-9955-b1499d5701d3')
->email->toBe('carter@example.com')
->count->toBe(3)
->clicked_links->toBe([
['url' => 'https://resend.com/pricing', 'clicks' => 2],
]);
});

it('can get a list of bounced broadcast recipients filtered by bounce type', function () {
$client = mockClient('GET', 'broadcasts/559ac32e-9ef5-46fb-82a1-b76b840c0f7b/recipients?type=bounced&bounce_type=permanent', [], [], broadcastBouncedRecipients());

$result = $client->broadcasts->recipients('559ac32e-9ef5-46fb-82a1-b76b840c0f7b', [
'type' => 'bounced',
'bounce_type' => 'permanent',
]);

expect($result)->toBeInstanceOf(Collection::class)
->has_more->toBeFalse();

expect($result->data[0])->toBeInstanceOf(Recipient::class)
->id->toBe('b2Zmc2V0OjE')
->contact_id->toBeNull()
->email->toBe('dana@example.com')
->bounce_type->toBe('permanent');
});

it('cannot get recipients for a broadcast that does not exist', function () {
/** @var Mockery\MockInterface|Transporter $transporter */
$transporter = Mockery::mock(Transporter::class);
$transporter->shouldReceive('request')->once()->andThrow(new ErrorException([
'statusCode' => 404,
'name' => 'not_found',
'message' => 'Broadcast not found',
]));

$client = new Client($transporter);

$client->broadcasts->recipients('559ac32e-9ef5-46fb-82a1-b76b840c0f7b', [
'type' => 'sent',
]);
})->throws(ErrorException::class, 'Broadcast not found');

it('can send a broadcast resource', function () {
$client = mockClient('POST', 'broadcasts/559ac32e-9ef5-46fb-82a1-b76b840c0f7b/send', [
'scheduled_at' => 'in 1 min',
Expand Down