From 97d099fbe0837a35de419a5c61a06e3762d87f15 Mon Sep 17 00:00:00 2001 From: Ivan Paskhin Date: Fri, 20 Sep 2024 09:01:04 -0400 Subject: [PATCH 1/5] feat: add segment management methods (CRUD, customer management) --- segments_api.go | 212 ++++++++++++ segments_api_test.go | 623 ++++++++++++++++++++++++++++++++++++ segments_customerio.go | 36 +++ segments_customerio_test.go | 169 ++++++++++ 4 files changed, 1040 insertions(+) create mode 100644 segments_api.go create mode 100644 segments_api_test.go create mode 100644 segments_customerio.go create mode 100644 segments_customerio_test.go diff --git a/segments_api.go b/segments_api.go new file mode 100644 index 0000000..0b7cbfc --- /dev/null +++ b/segments_api.go @@ -0,0 +1,212 @@ +package customerio + +import ( + "context" + "encoding/json" + "fmt" + "net/http" +) + +const errUnexpectedStatusCode = "unexpected status code %d" + +// SegmentState represents the possible states of a segment. +// Enum values: +// - events: currently handling event conditions for this segment +// - build: currently handling profile attribute conditions for this segment +// - events_queued: waiting to process event conditions for this segment +// - build_queued: waiting to process profile attribute conditions for this segment +// - finished: the segment has completed building +type SegmentState string + +const ( + SegmentStateEvents SegmentState = "events" + SegmentStateBuild SegmentState = "build" + SegmentStateEventsQueued SegmentState = "events_queued" + SegmentStateBuildQueued SegmentState = "build_queued" + SegmentStateFinished SegmentState = "finished" +) + +// SegmentType represents the type of a segment. +// Enum values: +// - dynamic: segment is dynamic, meaning it changes over time +// - manual: segment is manually maintained +type SegmentType string + +const ( + SegmentTypeDynamic SegmentType = "dynamic" + SegmentTypeManual SegmentType = "manual" +) + +// Segment represents a segment object returned by the API. +type Segment struct { + ID int `json:"id"` // Unique identifier for the segment. + DeduplicateID string `json:"deduplicate_id"` // A string in the format id:timestamp. + Name string `json:"name"` // Name of the segment. + Description string `json:"description"` // Description of the segment's purpose. + State SegmentState `json:"state"` // Current state of the segment. + Progress *int `json:"progress,omitempty"` // Progress percentage if the segment is building. + Type SegmentType `json:"type"` // Type of segment: dynamic or manual. + Tags []string `json:"tags,omitempty"` // Optional tags associated with the segment. +} + +// CreateSegmentRequest represents the payload to create a new segment. +type CreateSegmentRequest struct { + Segment Segment `json:"segment"` // Segment data to create. +} + +// CreateSegmentResponse represents the response body for a segment creation request. +type CreateSegmentResponse struct { + Segment Segment `json:"segment"` // The created segment. +} + +// CreateSegment sends a request to create a new segment and returns the created segment data. +func (c *APIClient) CreateSegment(ctx context.Context, req *CreateSegmentRequest) (*CreateSegmentResponse, error) { + body, statusCode, err := c.doRequest(ctx, "POST", "/v1/segments", req) + if err != nil { + return nil, err + } + if statusCode != http.StatusOK { + return nil, fmt.Errorf(errUnexpectedStatusCode, statusCode) + } + + var response CreateSegmentResponse + if err := json.Unmarshal(body, &response); err != nil { + return nil, err + } + return &response, nil +} + +// ListSegmentsResponse represents the response containing multiple segments. +type ListSegmentsResponse struct { + Segments []Segment `json:"segments"` // List of segments. +} + +// ListSegments retrieves all segments from the API. +func (c *APIClient) ListSegments(ctx context.Context) (*ListSegmentsResponse, error) { + respBody, statusCode, err := c.doRequest(ctx, "GET", "/v1/segments", nil) + if err != nil { + return nil, err + } + if statusCode != http.StatusOK { + return nil, fmt.Errorf(errUnexpectedStatusCode, statusCode) + } + + var response ListSegmentsResponse + if err := json.Unmarshal(respBody, &response); err != nil { + return nil, err + } + return &response, nil +} + +// GetSegmentResponse represents the response for retrieving a single segment. +type GetSegmentResponse struct { + Segment Segment `json:"segment"` // The requested segment. +} + +// GetSegment retrieves a specific segment by its ID. +func (c *APIClient) GetSegment(ctx context.Context, segmentID int) (*GetSegmentResponse, error) { + respBody, statusCode, err := c.doRequest(ctx, "GET", fmt.Sprintf("/v1/segments/%d", segmentID), nil) + if err != nil { + return nil, err + } + if statusCode != http.StatusOK { + return nil, fmt.Errorf(errUnexpectedStatusCode, statusCode) + } + + var response GetSegmentResponse + if err := json.Unmarshal(respBody, &response); err != nil { + return nil, err + } + return &response, nil +} + +// DeleteSegment removes a segment by its ID. +func (c *APIClient) DeleteSegment(ctx context.Context, segmentID int) error { + _, statusCode, err := c.doRequest(ctx, "DELETE", fmt.Sprintf("/v1/segments/%d", segmentID), nil) + if err != nil { + return err + } + if statusCode != http.StatusNoContent { + return fmt.Errorf(errUnexpectedStatusCode, statusCode) + } + return nil +} + +// GetSegmentDependenciesResponse represents the response containing segment dependencies. +type GetSegmentDependenciesResponse struct { + UsedBy struct { + Campaigns []int `json:"campaigns"` // List of campaigns using this segment. + SentNewsletters []int `json:"sent_newsletters"` // List of sent newsletters using this segment. + DraftNewsletters []int `json:"draft_newsletters"` // List of draft newsletters using this segment. + } `json:"used_by"` // Dependencies of the segment. +} + +// GetSegmentDependencies returns the dependencies of a specific segment. +func (c *APIClient) GetSegmentDependencies(ctx context.Context, segmentID int) (*GetSegmentDependenciesResponse, error) { + respBody, statusCode, err := c.doRequest(ctx, "GET", fmt.Sprintf("/v1/segments/%d/used_by", segmentID), nil) + if err != nil { + return nil, err + } + if statusCode != http.StatusOK { + return nil, fmt.Errorf(errUnexpectedStatusCode, statusCode) + } + + var response GetSegmentDependenciesResponse + if err := json.Unmarshal(respBody, &response); err != nil { + return nil, err + } + return &response, nil +} + +// GetSegmentCustomerCountResponse represents the response for retrieving customer count in a segment. +type GetSegmentCustomerCountResponse struct { + Count int `json:"count"` // Number of customers in the segment. +} + +// GetSegmentCustomerCount returns the total number of customers in a specific segment. +func (c *APIClient) GetSegmentCustomerCount(ctx context.Context, segmentID int) (*GetSegmentCustomerCountResponse, error) { + respBody, statusCode, err := c.doRequest(ctx, "GET", fmt.Sprintf("/v1/segments/%d/customer_count", segmentID), nil) + if err != nil { + return nil, err + } + if statusCode != http.StatusOK { + return nil, fmt.Errorf(errUnexpectedStatusCode, statusCode) + } + + var response GetSegmentCustomerCountResponse + if err := json.Unmarshal(respBody, &response); err != nil { + return nil, err + } + return &response, nil +} + +// ListCustomersInSegmentResponse represents the response for listing customers in a segment. +type ListCustomersInSegmentResponse struct { + IDs []string `json:"ids"` // List of customer IDs. + Identifiers []CustomerIdentifier `json:"identifiers"` // List of customer identifiers. + Next string `json:"next"` // Optional pagination cursor. +} + +// CustomerIdentifier represents the customer identifiers in a segment. +type CustomerIdentifier struct { + Email string `json:"email"` // Email of the customer. + ID int `json:"id"` // Internal ID of the customer. + CioID string `json:"cio_id"` // Customer.io ID of the customer. +} + +// ListCustomersInSegment retrieves a list of customers in a specific segment. +func (c *APIClient) ListCustomersInSegment(ctx context.Context, segmentID int) (*ListCustomersInSegmentResponse, error) { + respBody, statusCode, err := c.doRequest(ctx, "GET", fmt.Sprintf("/v1/segments/%d/membership", segmentID), nil) + if err != nil { + return nil, err + } + if statusCode != http.StatusOK { + return nil, fmt.Errorf(errUnexpectedStatusCode, statusCode) + } + + var response ListCustomersInSegmentResponse + if err := json.Unmarshal(respBody, &response); err != nil { + return nil, err + } + return &response, nil +} diff --git a/segments_api_test.go b/segments_api_test.go new file mode 100644 index 0000000..cbc8ecf --- /dev/null +++ b/segments_api_test.go @@ -0,0 +1,623 @@ +package customerio_test + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/customerio/go-customerio/v3" +) + +var ( + testSegmentID = 1 + notFoundID = 2 + testDeduplicateID = "deduplicate_id" +) + +func TestCreateSegment(t *testing.T) { + createSegmentRequest := &customerio.CreateSegmentRequest{ + Segment: customerio.Segment{ + Name: "name", + Description: "description", + State: "state", + Progress: intPtr(1), + Type: "type", + Tags: []string{"tags"}, + }, + } + + var verify = func(request []byte) { + var body customerio.CreateSegmentRequest + if err := json.Unmarshal(request, &body); err != nil { + t.Error(err) + } + + if !reflect.DeepEqual(&body, createSegmentRequest) { + t.Errorf("Request differed, want: %#v, got: %#v", request, body) + } + } + + api, srv := segmentsAppServer(t, verify) + defer srv.Close() + + resp, err := api.CreateSegment(context.Background(), createSegmentRequest) + if err != nil { + t.Error(err) + } + + expect := &customerio.CreateSegmentResponse{ + Segment: customerio.Segment{ + ID: testSegmentID, + DeduplicateID: testDeduplicateID, + }, + } + + if !reflect.DeepEqual(resp, expect) { + t.Errorf("Expect: %#v, Got: %#v", expect, resp) + } +} + +func TestCreateSegmentDoRequestError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + conn, _, _ := w.(http.Hijacker).Hijack() + conn.Close() + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + _, err := api.CreateSegment(context.Background(), &customerio.CreateSegmentRequest{ + Segment: customerio.Segment{ + Name: "name", + Description: "description", + State: "state", + Progress: intPtr(1), + Type: "type", + Tags: []string{"tags"}, + }, + }) + if err == nil { + t.Errorf("Expected error due to request failure, got: nil") + } +} + +func TestCreateSegmentNotOKError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(502) + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + _, err := api.CreateSegment(context.Background(), &customerio.CreateSegmentRequest{ + Segment: customerio.Segment{ + Name: "name", + Description: "description", + State: "state", + Progress: intPtr(1), + Type: "type", + Tags: []string{"tags"}, + }, + }) + if err == nil { + t.Errorf("Expected error, got: %#v", err) + } +} + +func TestCreateSegmentUnmarshalError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Write([]byte(`invalid-json`)) + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + _, err := api.CreateSegment(context.Background(), &customerio.CreateSegmentRequest{ + Segment: customerio.Segment{ + Name: "name", + Description: "description", + State: "state", + Progress: intPtr(1), + Type: "type", + Tags: []string{"tags"}, + }, + }) + + if err == nil { + t.Errorf("Expected error due to invalid JSON, got: nil") + } +} + +func TestListSegments(t *testing.T) { + var verify = func(request []byte) {} + + api, srv := segmentsAppServer(t, verify) + defer srv.Close() + + resp, err := api.ListSegments(context.Background()) + if err != nil { + t.Error(err) + } + + expect := &customerio.ListSegmentsResponse{ + Segments: []customerio.Segment{ + { + ID: testSegmentID, + DeduplicateID: testDeduplicateID, + }, + }, + } + + if !reflect.DeepEqual(resp, expect) { + t.Errorf("Expect: %#v, Got: %#v", expect, resp) + } +} + +func TestListSegmentsDoRequestError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + conn, _, _ := w.(http.Hijacker).Hijack() + conn.Close() + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + _, err := api.ListSegments(context.Background()) + if err == nil { + t.Errorf("Expected error due to request failure, got: nil") + } +} + +func TestListSegmentsNotOKError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(502) + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + _, err := api.ListSegments(context.Background()) + if err == nil { + t.Errorf("Expected error, got: %#v", err) + } +} + +func TestListSegmentsUnmarshalError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Write([]byte(`invalid-json`)) + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + _, err := api.ListSegments(context.Background()) + if err == nil { + t.Errorf("Expected error due to invalid JSON, got: nil") + } +} + +func TestGetSegment(t *testing.T) { + var verify = func(request []byte) {} + + api, srv := segmentsAppServer(t, verify) + defer srv.Close() + + resp, err := api.GetSegment(context.Background(), testSegmentID) + if err != nil { + t.Error(err) + } + + expect := &customerio.GetSegmentResponse{ + Segment: customerio.Segment{ + ID: testSegmentID, + DeduplicateID: testDeduplicateID, + }, + } + + if !reflect.DeepEqual(resp, expect) { + t.Errorf("Expect: %#v, Got: %#v", expect, resp) + } +} + +func TestGetSegmentDoRequestError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + conn, _, _ := w.(http.Hijacker).Hijack() + conn.Close() + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + _, err := api.GetSegment(context.Background(), testSegmentID) + if err == nil { + t.Errorf("Expected error due to request failure, got: nil") + } +} + +func TestGetSegmentNotOKError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(502) + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + _, err := api.GetSegment(context.Background(), testSegmentID) + if err == nil { + t.Errorf("Expected error, got: %#v", err) + } +} + +func TestGetSegmentUnmarshalError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Write([]byte(`invalid-json`)) + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + _, err := api.GetSegment(context.Background(), testSegmentID) + if err == nil { + t.Errorf("Expected error due to invalid JSON, got: nil") + } +} + +func TestDeleteSegment(t *testing.T) { + var verify = func(request []byte) {} + + api, srv := segmentsAppServer(t, verify) + defer srv.Close() + + err := api.DeleteSegment(context.Background(), testSegmentID) + if err != nil { + t.Error(err) + } +} + +func TestDeleteSegmentDoRequestError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + conn, _, _ := w.(http.Hijacker).Hijack() + conn.Close() + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + err := api.DeleteSegment(context.Background(), testSegmentID) + if err == nil { + t.Errorf("Expected error due to request failure, got: nil") + } +} + +func TestDeleteSegmentNotOKError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(502) + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + err := api.DeleteSegment(context.Background(), testSegmentID) + if err == nil { + t.Errorf("Expected error, got: %#v", err) + } +} + +func TestDeleteSegmentUnmarshalError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Write([]byte(`invalid-json`)) + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + err := api.DeleteSegment(context.Background(), testSegmentID) + if err == nil { + t.Errorf("Expected error due to invalid JSON, got: nil") + } +} + +func TestGetSegmentDependencies(t *testing.T) { + var verify = func(request []byte) {} + + api, srv := segmentsAppServer(t, verify) + defer srv.Close() + + resp, err := api.GetSegmentDependencies(context.Background(), testSegmentID) + if err != nil { + t.Error(err) + } + + expect := &customerio.GetSegmentDependenciesResponse{ + UsedBy: struct { + Campaigns []int `json:"campaigns"` + SentNewsletters []int `json:"sent_newsletters"` + DraftNewsletters []int `json:"draft_newsletters"` + }{ + Campaigns: []int{1}, + SentNewsletters: []int{2}, + DraftNewsletters: []int{3}, + }, + } + + if !reflect.DeepEqual(resp, expect) { + t.Errorf("Expect: %#v, Got: %#v", expect, resp) + } +} + +func TestGetSegmentDependenciesDoRequestError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + conn, _, _ := w.(http.Hijacker).Hijack() + conn.Close() + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + _, err := api.GetSegmentDependencies(context.Background(), testSegmentID) + if err == nil { + t.Errorf("Expected error due to request failure, got: nil") + } +} + +func TestGetSegmentDependenciesNotOKError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(502) + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + _, err := api.GetSegmentDependencies(context.Background(), testSegmentID) + if err == nil { + t.Errorf("Expected error, got: %#v", err) + } +} + +func TestGetSegmentDependenciesUnmarshalError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Write([]byte(`invalid-json`)) + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + _, err := api.GetSegmentDependencies(context.Background(), testSegmentID) + if err == nil { + t.Errorf("Expected error due to invalid JSON, got: nil") + } +} + +func TestGetSegmentCustomerCount(t *testing.T) { + var verify = func(request []byte) {} + + api, srv := segmentsAppServer(t, verify) + defer srv.Close() + + resp, err := api.GetSegmentCustomerCount(context.Background(), testSegmentID) + if err != nil { + t.Error(err) + } + + expect := &customerio.GetSegmentCustomerCountResponse{ + Count: 1, + } + + if !reflect.DeepEqual(resp, expect) { + t.Errorf("Expect: %#v, Got: %#v", expect, resp) + } +} + +func TestGetSegmentCustomerCountDoRequestError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + conn, _, _ := w.(http.Hijacker).Hijack() + conn.Close() + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + _, err := api.GetSegmentCustomerCount(context.Background(), testSegmentID) + if err == nil { + t.Errorf("Expected error due to request failure, got: nil") + } +} + +func TestGetSegmentCustomerCountNotOKError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(502) + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + _, err := api.GetSegmentCustomerCount(context.Background(), testSegmentID) + if err == nil { + t.Errorf("Expected error, got: %#v", err) + } +} + +func TestGetSegmentCustomerCountUnmarshalError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Write([]byte(`invalid-json`)) + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + _, err := api.GetSegmentCustomerCount(context.Background(), testSegmentID) + if err == nil { + t.Errorf("Expected error due to invalid JSON, got: nil") + } +} + +func TestListCustomersInSegment(t *testing.T) { + var verify = func(request []byte) {} + + api, srv := segmentsAppServer(t, verify) + defer srv.Close() + + resp, err := api.ListCustomersInSegment(context.Background(), testSegmentID) + if err != nil { + t.Error(err) + } + + expect := &customerio.ListCustomersInSegmentResponse{ + IDs: []string{"string"}, + Identifiers: []customerio.CustomerIdentifier{ + { + Email: "test@example.com", + ID: 2, + CioID: "a3000001", + }, + }, + Next: "string", + } + + if !reflect.DeepEqual(resp, expect) { + t.Errorf("Expect: %#v, Got: %#v", expect, resp) + } +} + +func TestListCustomersInSegmentDoRequestError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + conn, _, _ := w.(http.Hijacker).Hijack() + conn.Close() + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + _, err := api.ListCustomersInSegment(context.Background(), testSegmentID) + if err == nil { + t.Errorf("Expected error due to request failure, got: nil") + } +} + +func TestListCustomersInSegmentNotOKError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(502) + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + _, err := api.ListCustomersInSegment(context.Background(), testSegmentID) + if err == nil { + t.Errorf("Expected error, got: %#v", err) + } +} + +func TestListCustomersInSegmentUnmarshalError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Write([]byte(`invalid-json`)) + })) + defer srv.Close() + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + _, err := api.ListCustomersInSegment(context.Background(), testSegmentID) + if err == nil { + t.Errorf("Expected error due to invalid JSON, got: nil") + } +} + +func intPtr(s int) *int { + return &s +} + +func segmentsAppServer(t *testing.T, verify func(request []byte)) (*customerio.APIClient, *httptest.Server) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + b, err := ioutil.ReadAll(req.Body) + if err != nil { + t.Error(err) + } + defer req.Body.Close() + + verify(b) + + switch true { + case req.Method == "POST" && req.URL.Path == "/v1/segments": + w.Write([]byte(`{ + "segment": { + "id": ` + fmt.Sprintf("%d", testSegmentID) + `, + "deduplicate_id": "` + testDeduplicateID + `" + } + }`)) + case req.Method == "GET" && req.URL.Path == "/v1/segments": + w.Write([]byte(`{ + "segments": [ + { + "id": ` + fmt.Sprintf("%d", testSegmentID) + `, + "deduplicate_id": "` + testDeduplicateID + `" + } + ]}`)) + case req.Method == "GET" && req.URL.Path == "/v1/segments/1": + w.Write([]byte(`{ + "segment": { + "id": ` + fmt.Sprintf("%d", testSegmentID) + `, + "deduplicate_id": "` + testDeduplicateID + `" + } + }`)) + case req.Method == "DELETE" && req.URL.Path == "/v1/segments/1": + w.WriteHeader(http.StatusNoContent) + case req.Method == "GET" && req.URL.Path == "/v1/segments/1/customer_count": + w.Write([]byte(`{ + "count": 1 + }`)) + case req.Method == "GET" && req.URL.Path == "/v1/segments/1/membership": + w.Write([]byte(`{ + "ids": ["string"], + "identifiers": [ + { + "email": "test@example.com", + "id": 2, + "cio_id": "a3000001" + } + ], + "next": "string" + }`)) + case req.Method == "GET" && req.URL.Path == "/v1/segments/1/used_by": + w.Write([]byte(`{ + "used_by": { + "campaigns": [1], + "sent_newsletters": [2], + "draft_newsletters": [3] + } + }`)) + default: + t.Errorf("Unexpected request: %s %s", req.Method, req.URL.Path) + } + })) + + api := customerio.NewAPIClient("myKey") + api.URL = srv.URL + + return api, srv +} diff --git a/segments_customerio.go b/segments_customerio.go new file mode 100644 index 0000000..8e5b295 --- /dev/null +++ b/segments_customerio.go @@ -0,0 +1,36 @@ +package customerio + +import ( + "context" + "fmt" +) + +// AddPeopleToSegment adds people to a segment. +func (c *CustomerIO) AddPeopleToSegment(ctx context.Context, segmentID int, ids []string) error { + if segmentID == 0 { + return ParamError{Param: "segmentID"} + } + if len(ids) == 0 { + return ParamError{Param: "ids"} + } + return c.request(ctx, "POST", + fmt.Sprintf("%s/api/v1/segments/%d/add_customers", c.URL, segmentID), + map[string]interface{}{ + "ids": ids, + }) +} + +// RemovePeopleFromSegment removes people from a segment +func (c *CustomerIO) RemovePeopleFromSegment(ctx context.Context, segmentID int, ids []string) error { + if segmentID == 0 { + return ParamError{Param: "segmentID"} + } + if len(ids) == 0 { + return ParamError{Param: "ids"} + } + return c.request(ctx, "DELETE", + fmt.Sprintf("%s/api/v1/segments/%d/remove_customers", c.URL, segmentID), + map[string]interface{}{ + "ids": ids, + }) +} diff --git a/segments_customerio_test.go b/segments_customerio_test.go new file mode 100644 index 0000000..b9b4567 --- /dev/null +++ b/segments_customerio_test.go @@ -0,0 +1,169 @@ +package customerio_test + +import ( + "context" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/customerio/go-customerio/v3" +) + +func TestAddPeopleToSegment(t *testing.T) { + customerIDs := []string{"1", "2", "3"} + var verify = func(request []byte) {} + + api, srv := segmentsTrackServer(t, verify) + defer srv.Close() + + err := api.AddPeopleToSegment(context.Background(), testSegmentID, customerIDs) + if err != nil { + t.Error(err) + } +} + +func TestAddPeopleToSegmentSegmentParamError(t *testing.T) { + var customerIDs []string + var verify = func(request []byte) {} + + api, srv := segmentsTrackServer(t, verify) + defer srv.Close() + + err := api.AddPeopleToSegment(context.Background(), 0, customerIDs) + if err == nil { + t.Errorf("Expected error, got: %#v", err) + } + + if e, ok := err.(customerio.ParamError); !ok { + t.Errorf("Expected ParamError, got: %#v", e) + } +} + +func TestAddPeopleToSegmentIDsParamError(t *testing.T) { + var customerIDs []string + var verify = func(request []byte) {} + + api, srv := segmentsTrackServer(t, verify) + defer srv.Close() + + err := api.AddPeopleToSegment(context.Background(), testSegmentID, customerIDs) + if err == nil { + t.Errorf("Expected error, got: %#v", err) + } + + if e, ok := err.(customerio.ParamError); !ok { + t.Errorf("Expected ParamError, got: %#v", e) + } +} + +func TestAddPeopleToSegmentError(t *testing.T) { + customerIDs := []string{"1", "2", "3"} + var verify = func(request []byte) {} + + api, srv := segmentsTrackServer(t, verify) + defer srv.Close() + + err := api.AddPeopleToSegment(context.Background(), notFoundID, customerIDs) + if err == nil { + t.Errorf("Expected error, got: %#v", err) + } + + if e, ok := err.(*customerio.CustomerIOError); !ok { + t.Errorf("Expected CustomerIOError, got: %#v", e) + } +} + +func segmentsTrackServer(t *testing.T, verify func(request []byte)) (*customerio.CustomerIO, *httptest.Server) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + b, err := ioutil.ReadAll(req.Body) + if err != nil { + t.Error(err) + } + defer req.Body.Close() + + verify(b) + + switch true { + case req.Method == "POST" && req.URL.Path == "/api/v1/segments/1/add_customers": + w.WriteHeader(http.StatusOK) + case req.Method == "POST" && req.URL.Path == "/api/v1/segments/2/add_customers": + w.WriteHeader(http.StatusNotFound) + case req.Method == "DELETE" && req.URL.Path == "/api/v1/segments/1/remove_customers": + w.WriteHeader(http.StatusOK) + case req.Method == "DELETE" && req.URL.Path == "/api/v1/segments/2/remove_customers": + w.WriteHeader(http.StatusNotFound) + default: + t.Errorf("Unexpected request: %s %s", req.Method, req.URL.Path) + } + })) + + api := customerio.NewCustomerIO("test", "myKey") + api.URL = srv.URL + + return api, srv +} + +func TestRemovePeopleFromSegment(t *testing.T) { + customerIDs := []string{"1", "2", "3"} + var verify = func(request []byte) {} + + api, srv := segmentsTrackServer(t, verify) + defer srv.Close() + + err := api.RemovePeopleFromSegment(context.Background(), testSegmentID, customerIDs) + if err != nil { + t.Error(err) + } +} + +func TestRemovePeopleFromSegmentSegmentParamError(t *testing.T) { + var customerIDs []string + var verify = func(request []byte) {} + + api, srv := segmentsTrackServer(t, verify) + defer srv.Close() + + err := api.RemovePeopleFromSegment(context.Background(), 0, customerIDs) + if err == nil { + t.Errorf("Expected error, got: %#v", err) + } + + if e, ok := err.(customerio.ParamError); !ok { + t.Errorf("Expected ParamError, got: %#v", e) + } +} + +func TestRemovePeopleFromSegmentIDsParamError(t *testing.T) { + var customerIDs []string + var verify = func(request []byte) {} + + api, srv := segmentsTrackServer(t, verify) + defer srv.Close() + + err := api.RemovePeopleFromSegment(context.Background(), testSegmentID, customerIDs) + if err == nil { + t.Errorf("Expected error, got: %#v", err) + } + + if e, ok := err.(customerio.ParamError); !ok { + t.Errorf("Expected ParamError, got: %#v", e) + } +} + +func TestRemovePeopleFromSegmentError(t *testing.T) { + customerIDs := []string{"1", "2", "3"} + var verify = func(request []byte) {} + + api, srv := segmentsTrackServer(t, verify) + defer srv.Close() + + err := api.RemovePeopleFromSegment(context.Background(), notFoundID, customerIDs) + if err == nil { + t.Errorf("Expected error, got: %#v", err) + } + + if e, ok := err.(*customerio.CustomerIOError); !ok { + t.Errorf("Expected CustomerIOError, got: %#v", e) + } +} From 4a8327177357f42f6409b367e099c89f37100db3 Mon Sep 17 00:00:00 2001 From: Ivan Paskhin Date: Fri, 20 Sep 2024 09:56:31 -0400 Subject: [PATCH 2/5] docs: add usage examples for segment management methods in README --- README.md | 5 ++ SEGMENTS.md | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 SEGMENTS.md diff --git a/README.md b/README.md index 0fd0af4..b17885f 100644 --- a/README.md +++ b/README.md @@ -388,6 +388,11 @@ if err := track.TrackCtx(ctx, "5", "purchase", map[string]interface{}{ } ``` +## Segments API + +We also provide a client for managing customer segments through the Segments API. For more details on how to use it, refer to the [Segments API README](./SEGMENTS.md). + + ## Contributing 1. Fork it diff --git a/SEGMENTS.md b/SEGMENTS.md new file mode 100644 index 0000000..053857e --- /dev/null +++ b/SEGMENTS.md @@ -0,0 +1,176 @@ +# Customer.io Segments API Methods + +This document explains how to use the new methods added for managing customer segments in Customer.io. These methods enable you to list, create, delete segments, and manage customers in segments. + +## Usage + +### Available Methods + +1. **Create Segment** + + Creates a new segment in Customer.io. + + ```go + request := &CreateSegmentRequest{ + Segment: Segment{ + Name: "New Segment", + }, + } + + resp, err := cio.CreateSegment(context.Background(), request) + if err != nil { + log.Fatal(err) + } + + log.Printf("Segment created: ID: %d, Name: %s", resp.Segment.ID, resp.Segment.Name) + ``` + +2. **List Segments** + + Retrieves a list of all customer segments. + + ```go + segments, err := cio.ListSegments(context.Background()) + if err != nil { + log.Fatal(err) + } + + for _, segment := range segments.Segments { + log.Printf("Segment ID: %d, Name: %s", segment.ID, segment.Name) + } + ``` + +3. **Get Segment by ID** + + Retrieves a specific segment by its ID. + + ```go + segmentID := 1234 + resp, err := cio.GetSegment(context.Background(), segmentID) + if err != nil { + log.Fatal(err) + } + + log.Printf("Segment ID: %d, Name: %s", resp.Segment.ID, resp.Segment.Name) + ``` + +4. **Delete Segment** + + Deletes a segment by its ID. + + ```go + segmentID := 1234 + err := cio.DeleteSegment(context.Background(), segmentID) + if err != nil { + log.Fatal(err) + } + + log.Println("Segment deleted successfully") + ``` + +5. **Get Segment Dependencies** + + Retrieves dependencies for a specific segment. + + ```go + segmentID := 1234 + dependencies, err := cio.GetSegmentDependencies(context.Background(), segmentID) + if err != nil { + log.Fatal(err) + } + + log.Printf("Segment dependencies: %v", dependencies) + ``` + +6. **Get Segment Customer Count** + + Retrieves the number of customers in a specific segment. + + ```go + segmentID := 1234 + count, err := cio.GetSegmentCustomerCount(context.Background(), segmentID) + if err != nil { + log.Fatal(err) + } + + log.Printf("Customer count in segment %d: %d", segmentID, count.Count) + ``` + +7. **List Customers in a Segment** + + Retrieves a list of customers in a specific segment. + + ```go + segmentID := 1234 + customers, err := cio.ListCustomersInSegment(context.Background(), segmentID) + if err != nil { + log.Fatal(err) + } + + for _, customer := range customers.Identifiers { + log.Printf("Customer ID: %d", customer.ID) + } + ``` + +### Managing Customers in Segments + +You can add or remove customers from segments using the following methods: + +1. **Add People to Segment** + + Adds a list of customer IDs to a segment. + + ```go + segmentID := 1234 + customerIDs := []string{"customer_1", "customer_2"} + + err := track.AddPeopleToSegment(context.Background(), segmentID, customerIDs) + if err != nil { + log.Fatal(err) + } + + log.Println("Customers added to segment successfully") + ``` + +2. **Remove People from Segment** + + Removes a list of customer IDs from a segment. + + ```go + segmentID := 1234 + customerIDs := []string{"customer_1", "customer_2"} + + err := track.RemovePeopleFromSegment(context.Background(), segmentID, customerIDs) + if err != nil { + log.Fatal(err) + } + + log.Println("Customers removed from segment successfully") + ``` + +## Example: Creating a Segment and Adding Customers + +```go +func main() { + // Create a new segment + request := &CreateSegmentRequest{ + Segment: Segment{ + Name: "VIP Customers", + }, + } + resp, err := cio.CreateSegment(context.Background(), request) + if err != nil { + log.Fatal(err) + } + + log.Printf("Created segment: %s (ID: %d)", resp.Segment.Name, resp.Segment.ID) + + // Add customers to the new segment + customerIDs := []string{"customer_1", "customer_2"} + err = track.AddPeopleToSegment(context.Background(), resp.Segment.ID, customerIDs) + if err != nil { + log.Fatal(err) + } + + log.Println("Customers added to the segment successfully") +} From af3466562d1ba1ca727a83e093fca35c7551cfd3 Mon Sep 17 00:00:00 2001 From: Ivan Paskhin Date: Mon, 23 Sep 2024 17:22:11 -0400 Subject: [PATCH 3/5] fix: change RemovePeopleFromSegment method from DELETE to POST --- segments_customerio.go | 2 +- segments_customerio_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/segments_customerio.go b/segments_customerio.go index 8e5b295..3c60536 100644 --- a/segments_customerio.go +++ b/segments_customerio.go @@ -28,7 +28,7 @@ func (c *CustomerIO) RemovePeopleFromSegment(ctx context.Context, segmentID int, if len(ids) == 0 { return ParamError{Param: "ids"} } - return c.request(ctx, "DELETE", + return c.request(ctx, "POST", fmt.Sprintf("%s/api/v1/segments/%d/remove_customers", c.URL, segmentID), map[string]interface{}{ "ids": ids, diff --git a/segments_customerio_test.go b/segments_customerio_test.go index b9b4567..51d0ce9 100644 --- a/segments_customerio_test.go +++ b/segments_customerio_test.go @@ -89,9 +89,9 @@ func segmentsTrackServer(t *testing.T, verify func(request []byte)) (*customerio w.WriteHeader(http.StatusOK) case req.Method == "POST" && req.URL.Path == "/api/v1/segments/2/add_customers": w.WriteHeader(http.StatusNotFound) - case req.Method == "DELETE" && req.URL.Path == "/api/v1/segments/1/remove_customers": + case req.Method == "POST" && req.URL.Path == "/api/v1/segments/1/remove_customers": w.WriteHeader(http.StatusOK) - case req.Method == "DELETE" && req.URL.Path == "/api/v1/segments/2/remove_customers": + case req.Method == "POST" && req.URL.Path == "/api/v1/segments/2/remove_customers": w.WriteHeader(http.StatusNotFound) default: t.Errorf("Unexpected request: %s %s", req.Method, req.URL.Path) From 4b00831006a0f700f3ed34e7aeabaaf7a4b6a1fe Mon Sep 17 00:00:00 2001 From: Ivan Paskhin Date: Tue, 24 Sep 2024 13:32:37 -0400 Subject: [PATCH 4/5] fix: avoid sending null body in HTTP requests when body is nil --- api.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/api.go b/api.go index 8b74f59..b39f269 100644 --- a/api.go +++ b/api.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "io" "io/ioutil" "net/http" ) @@ -32,12 +33,17 @@ func NewAPIClient(key string, opts ...option) *APIClient { } func (c *APIClient) doRequest(ctx context.Context, verb, requestPath string, body interface{}) ([]byte, int, error) { - b, err := json.Marshal(body) - if err != nil { - return nil, 0, err + var requestBody io.Reader + + if body != nil { + b, err := json.Marshal(body) + if err != nil { + return nil, 0, err + } + requestBody = bytes.NewBuffer(b) } - req, err := http.NewRequest(verb, c.URL+requestPath, bytes.NewBuffer(b)) + req, err := http.NewRequest(verb, c.URL+requestPath, requestBody) if err != nil { return nil, 0, err } From 3c6c237f12ab2e002e4e034fcd78baba97895b96 Mon Sep 17 00:00:00 2001 From: Ivan Paskhin Date: Tue, 24 Sep 2024 14:23:18 -0400 Subject: [PATCH 5/5] fix: change CustomerIdentifier.ID type from int to string for consistency --- segments_api.go | 2 +- segments_api_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/segments_api.go b/segments_api.go index 0b7cbfc..9b18e8c 100644 --- a/segments_api.go +++ b/segments_api.go @@ -190,7 +190,7 @@ type ListCustomersInSegmentResponse struct { // CustomerIdentifier represents the customer identifiers in a segment. type CustomerIdentifier struct { Email string `json:"email"` // Email of the customer. - ID int `json:"id"` // Internal ID of the customer. + ID string `json:"id"` // Internal ID of the customer. CioID string `json:"cio_id"` // Customer.io ID of the customer. } diff --git a/segments_api_test.go b/segments_api_test.go index cbc8ecf..42ef893 100644 --- a/segments_api_test.go +++ b/segments_api_test.go @@ -490,7 +490,7 @@ func TestListCustomersInSegment(t *testing.T) { Identifiers: []customerio.CustomerIdentifier{ { Email: "test@example.com", - ID: 2, + ID: "test@example.com", CioID: "a3000001", }, }, @@ -597,7 +597,7 @@ func segmentsAppServer(t *testing.T, verify func(request []byte)) (*customerio.A "identifiers": [ { "email": "test@example.com", - "id": 2, + "id": "test@example.com", "cio_id": "a3000001" } ],