diff --git a/lib/resend/broadcasts.rb b/lib/resend/broadcasts.rb index 4459356..3959880 100644 --- a/lib/resend/broadcasts.rb +++ b/lib/resend/broadcasts.rb @@ -57,6 +57,25 @@ def get(broadcast_id = "") path = "broadcasts/#{broadcast_id}" Resend::Request.new(path, {}, "get").perform end + + # https://resend.com/docs/api-reference/broadcasts/list-broadcast-recipients + # @param broadcast_id [String] the broadcast id + # @param params [Hash] the parameters + # @option params [String] :type the recipient event type to filter by (required): + # sent, delivered, opened, clicked, bounced, complained, unsubscribed, suppressed + # @option params [String] :email filter recipients by email address (optional) + # @option params [String] :bounce_type filter bounced recipients by bounce type (optional, + # only valid when type is bounced): permanent, transient, undetermined + # @option params [Integer] :limit the maximum number of results to return (optional) + # @option params [String] :after the cursor for pagination (optional) + # @option params [String] :before the cursor for pagination (optional) + def recipients(broadcast_id = "", params = {}) + raise ArgumentError, "type is required" if params[:type].nil? + + base_path = "broadcasts/#{broadcast_id}/recipients" + path = Resend::PaginationHelper.build_paginated_path(base_path, params) + Resend::Request.new(path, {}, "get").perform + end end end end diff --git a/spec/broadcasts_spec.rb b/spec/broadcasts_spec.rb index d9e7a3c..f5a2f7a 100644 --- a/spec/broadcasts_spec.rb +++ b/spec/broadcasts_spec.rb @@ -168,6 +168,141 @@ end end + describe "recipients" do + it "should raise when type is missing" do + expect do + Resend::Broadcasts.recipients("559ac32e-9ef5-46fb-82a1-b76b840c0f7b", {}) + end.to raise_error(ArgumentError, "type is required") + end + + it "should list recipients for a basic event type" do + resp = { + "object": "list", + "has_more": false, + "data": [ + { + "id" => "b2Zmc2V0OjA", + "contact_id" => "e169aa45-1ecf-4183-9955-b1499d5701d3", + "email" => "carter@example.com" + } + ] + } + expect(Resend::Request).to receive(:new).with( + "broadcasts/559ac32e-9ef5-46fb-82a1-b76b840c0f7b/recipients?type=sent", + {}, + "get" + ).and_call_original + allow_any_instance_of(Resend::Request).to receive(:perform).and_return(resp) + + recipients = Resend::Broadcasts.recipients( + "559ac32e-9ef5-46fb-82a1-b76b840c0f7b", + { type: "sent" } + ) + + expect(recipients[:object]).to eql("list") + expect(recipients[:has_more]).to eql(false) + expect(recipients[:data].length).to eql(1) + expect(recipients[:data][0]["id"]).to eql("b2Zmc2V0OjA") + expect(recipients[:data][0]["contact_id"]).to eql("e169aa45-1ecf-4183-9955-b1499d5701d3") + expect(recipients[:data][0]["email"]).to eql("carter@example.com") + end + + it "should list recipients with count for opened type" do + resp = { + "object": "list", + "has_more": false, + "data": [ + { + "id" => "b2Zmc2V0OjA", + "contact_id" => "e169aa45-1ecf-4183-9955-b1499d5701d3", + "email" => "carter@example.com", + "count" => 3 + } + ] + } + allow_any_instance_of(Resend::Request).to receive(:perform).and_return(resp) + + recipients = Resend::Broadcasts.recipients( + "559ac32e-9ef5-46fb-82a1-b76b840c0f7b", + { type: "opened" } + ) + + expect(recipients[:data][0]["count"]).to eql(3) + end + + it "should list recipients with clicked_links for clicked type" do + resp = { + "object": "list", + "has_more": false, + "data": [ + { + "id" => "b2Zmc2V0OjA", + "contact_id" => "e169aa45-1ecf-4183-9955-b1499d5701d3", + "email" => "carter@example.com", + "count" => 2, + "clicked_links" => [ + { "url" => "https://resend.com/pricing", "clicks" => 2 } + ] + } + ] + } + allow_any_instance_of(Resend::Request).to receive(:perform).and_return(resp) + + recipients = Resend::Broadcasts.recipients( + "559ac32e-9ef5-46fb-82a1-b76b840c0f7b", + { type: "clicked" } + ) + + expect(recipients[:data][0]["count"]).to eql(2) + expect(recipients[:data][0]["clicked_links"]).to eql( + [{ "url" => "https://resend.com/pricing", "clicks" => 2 }] + ) + end + + it "should list recipients with bounce_type for bounced type" do + resp = { + "object": "list", + "has_more": false, + "data": [ + { + "id" => "b2Zmc2V0OjA", + "contact_id" => nil, + "email" => "carter@example.com", + "bounce_type" => "permanent" + } + ] + } + expect(Resend::Request).to receive(:new).with( + "broadcasts/559ac32e-9ef5-46fb-82a1-b76b840c0f7b/recipients?type=bounced&bounce_type=permanent", + {}, + "get" + ).and_call_original + allow_any_instance_of(Resend::Request).to receive(:perform).and_return(resp) + + recipients = Resend::Broadcasts.recipients( + "559ac32e-9ef5-46fb-82a1-b76b840c0f7b", + { type: "bounced", bounce_type: "permanent" } + ) + + expect(recipients[:data][0]["contact_id"]).to eql(nil) + expect(recipients[:data][0]["bounce_type"]).to eql("permanent") + end + + it "should raise when broadcast is not found" do + resp = { + "statusCode" => 404, + "name" => "not_found", + "message" => "Broadcast not found" + } + allow(resp).to receive(:body).and_return(resp) + allow(HTTParty).to receive(:send).and_return(resp) + + expect do + Resend::Broadcasts.recipients("missing-id", { type: "sent" }) + end.to raise_error(Resend::Error::NotFoundError, /Broadcast not found/) + end + end + describe "get broadcast" do it "should retrieve a broadcast" do