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
104 changes: 104 additions & 0 deletions cmd/contacts_suppression.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package cmd

import (
"fmt"

"github.com/loops-so/cli/internal/api"
"github.com/loops-so/cli/internal/config"
"github.com/spf13/cobra"
)

var contactsSuppressionCmd = &cobra.Command{
Use: "suppression",
Short: "Manage contact suppression",
}

// check

func runContactsSuppressionCheck(cfg *config.Config, email, userID string) (*api.ContactSuppression, error) {
return newAPIClient(cfg).CheckContactSuppression(email, userID)
}

var contactsSuppressionCheckCmd = &cobra.Command{
Use: "check",
Short: "Check suppression status for a contact",
RunE: func(cmd *cobra.Command, args []string) error {
email, _ := cmd.Flags().GetString("email")
userID, _ := cmd.Flags().GetString("user-id")

if (email == "") == (userID == "") {
return fmt.Errorf("exactly one of --email or --user-id is required")
}

cfg, err := loadConfig()
if err != nil {
return err
}

result, err := runContactsSuppressionCheck(cfg, email, userID)
if err != nil {
return err
}

if isJSONOutput() {
return printJSON(cmd.OutOrStdout(), result)
}

suppressed := "no"
if result.IsSuppressed {
suppressed = "yes"
}
fmt.Fprintf(cmd.OutOrStdout(), "Suppressed: %s\n", suppressed)
fmt.Fprintf(cmd.OutOrStdout(), "Removal quota: %d/%d remaining\n", result.RemovalQuota.Remaining, result.RemovalQuota.Limit)
return nil
},
}

// remove

func runContactsSuppressionRemove(cfg *config.Config, email, userID string) (*api.ContactSuppressionRemoval, error) {
return newAPIClient(cfg).RemoveContactSuppression(email, userID)
}

var contactsSuppressionRemoveCmd = &cobra.Command{
Use: "remove",
Short: "Remove a contact from the suppression list",
RunE: func(cmd *cobra.Command, args []string) error {
email, _ := cmd.Flags().GetString("email")
userID, _ := cmd.Flags().GetString("user-id")

if (email == "") == (userID == "") {
return fmt.Errorf("exactly one of --email or --user-id is required")
}

cfg, err := loadConfig()
if err != nil {
return err
}

result, err := runContactsSuppressionRemove(cfg, email, userID)
if err != nil {
return err
}

if isJSONOutput() {
return printJSON(cmd.OutOrStdout(), result)
}

fmt.Fprintln(cmd.OutOrStdout(), result.Message)
fmt.Fprintf(cmd.OutOrStdout(), "Removal quota: %d/%d remaining\n", result.RemovalQuota.Remaining, result.RemovalQuota.Limit)
return nil
},
}

func init() {
contactsSuppressionCheckCmd.Flags().StringP("email", "e", "", "Contact email address")
contactsSuppressionCheckCmd.Flags().StringP("user-id", "u", "", "Contact user ID")
contactsSuppressionCmd.AddCommand(contactsSuppressionCheckCmd)

contactsSuppressionRemoveCmd.Flags().StringP("email", "e", "", "Contact email address")
contactsSuppressionRemoveCmd.Flags().StringP("user-id", "u", "", "Contact user ID")
contactsSuppressionCmd.AddCommand(contactsSuppressionRemoveCmd)

contactsCmd.AddCommand(contactsSuppressionCmd)
}
64 changes: 64 additions & 0 deletions cmd/contacts_suppression_check_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package cmd

import (
"net/http"
"testing"
)

func TestRunContactsSuppressionCheck(t *testing.T) {
body := `{"contact":{"id":"cnt_abc123","email":"bob@example.com","userId":"user_123"},"isSuppressed":true,"removalQuota":{"limit":10,"remaining":8}}`

t.Run("checks by email", func(t *testing.T) {
serveJSON(t, http.StatusOK, body)
result, err := runContactsSuppressionCheck(cfg(t), "bob@example.com", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Contact.ID != "cnt_abc123" ||
result.Contact.Email != "bob@example.com" ||
deref(result.Contact.UserID) != "user_123" ||
!result.IsSuppressed ||
result.RemovalQuota.Limit != 10 ||
result.RemovalQuota.Remaining != 8 {
t.Errorf("unexpected result: %+v", result)
}
})

t.Run("checks by user ID", func(t *testing.T) {
serveJSON(t, http.StatusOK, body)
result, err := runContactsSuppressionCheck(cfg(t), "", "user_123")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !result.IsSuppressed {
t.Errorf("expected suppressed=true, got false")
}
})

t.Run("not suppressed", func(t *testing.T) {
serveJSON(t, http.StatusOK, `{"contact":{"id":"cnt_abc123","email":"bob@example.com"},"isSuppressed":false,"removalQuota":{"limit":10,"remaining":10}}`)
result, err := runContactsSuppressionCheck(cfg(t), "bob@example.com", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.IsSuppressed {
t.Errorf("expected suppressed=false, got true")
}
})

t.Run("returns error on 404", func(t *testing.T) {
serveJSON(t, http.StatusNotFound, `{"message":"Contact not found."}`)
_, err := runContactsSuppressionCheck(cfg(t), "nobody@example.com", "")
if err == nil {
t.Fatal("expected error, got nil")
}
})

t.Run("returns error on non-200 response", func(t *testing.T) {
serveJSON(t, http.StatusUnauthorized, `{"error":"unauthorized"}`)
_, err := runContactsSuppressionCheck(cfg(t), "bob@example.com", "")
if err == nil {
t.Fatal("expected error, got nil")
}
})
}
59 changes: 59 additions & 0 deletions cmd/contacts_suppression_remove_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package cmd

import (
"net/http"
"testing"
)

func TestRunContactsSuppressionRemove(t *testing.T) {
body := `{"success":true,"message":"Email removed from suppression list.","removalQuota":{"limit":10,"remaining":7}}`

t.Run("removes by email", func(t *testing.T) {
serveJSON(t, http.StatusOK, body)
result, err := runContactsSuppressionRemove(cfg(t), "bob@example.com", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !result.Success ||
result.Message != "Email removed from suppression list." ||
result.RemovalQuota.Limit != 10 ||
result.RemovalQuota.Remaining != 7 {
t.Errorf("unexpected result: %+v", result)
}
})

t.Run("removes by user ID", func(t *testing.T) {
serveJSON(t, http.StatusOK, body)
result, err := runContactsSuppressionRemove(cfg(t), "", "user_123")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !result.Success {
t.Errorf("expected success=true")
}
})

t.Run("returns error when not suppressed", func(t *testing.T) {
serveJSON(t, http.StatusBadRequest, `{"message":"Contact is not suppressed."}`)
_, err := runContactsSuppressionRemove(cfg(t), "bob@example.com", "")
if err == nil {
t.Fatal("expected error, got nil")
}
})

t.Run("returns error on 404", func(t *testing.T) {
serveJSON(t, http.StatusNotFound, `{"message":"Contact not found."}`)
_, err := runContactsSuppressionRemove(cfg(t), "nobody@example.com", "")
if err == nil {
t.Fatal("expected error, got nil")
}
})

t.Run("returns error on non-200 response", func(t *testing.T) {
serveJSON(t, http.StatusUnauthorized, `{"error":"unauthorized"}`)
_, err := runContactsSuppressionRemove(cfg(t), "bob@example.com", "")
if err == nil {
t.Fatal("expected error, got nil")
}
})
}
88 changes: 88 additions & 0 deletions internal/api/contacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,94 @@ func (c *Client) DeleteContact(email, userID string) error {
return nil
}

type ContactSuppression struct {
Contact struct {
ID string `json:"id"`
Email string `json:"email"`
UserID *string `json:"userId"`
} `json:"contact"`
IsSuppressed bool `json:"isSuppressed"`
RemovalQuota struct {
Limit int `json:"limit"`
Remaining int `json:"remaining"`
} `json:"removalQuota"`
}

type ContactSuppressionRemoval struct {
Success bool `json:"success"`
Message string `json:"message"`
RemovalQuota struct {
Limit int `json:"limit"`
Remaining int `json:"remaining"`
} `json:"removalQuota"`
}

func (c *Client) CheckContactSuppression(email, userID string) (*ContactSuppression, error) {
req, err := c.newRequest(http.MethodGet, "/contacts/suppression", nil)
if err != nil {
return nil, err
}

q := req.URL.Query()
if email != "" {
q.Set("email", email)
}
if userID != "" {
q.Set("userId", userID)
}
req.URL.RawQuery = q.Encode()

resp, err := c.do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, errorFromResponse(resp)
}

var result ContactSuppression
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}

return &result, nil
}

func (c *Client) RemoveContactSuppression(email, userID string) (*ContactSuppressionRemoval, error) {
req, err := c.newRequest(http.MethodDelete, "/contacts/suppression", nil)
if err != nil {
return nil, err
}

q := req.URL.Query()
if email != "" {
q.Set("email", email)
}
if userID != "" {
q.Set("userId", userID)
}
req.URL.RawQuery = q.Encode()

resp, err := c.do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, errorFromResponse(resp)
}

var result ContactSuppressionRemoval
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}

return &result, nil
}

type FindContactParams struct {
Email string
UserID string
Expand Down
Loading