diff --git a/cmd/campaigns.go b/cmd/campaigns.go index 4d72d67..c13af82 100644 --- a/cmd/campaigns.go +++ b/cmd/campaigns.go @@ -2,29 +2,23 @@ package cmd import ( "fmt" - "strings" "github.com/loops-so/cli/internal/api" "github.com/loops-so/cli/internal/config" "github.com/spf13/cobra" ) -func fromEmailUsername(s string) string { - before, _, _ := strings.Cut(s, "@") - return before -} - func runCampaignsGet(cfg *config.Config, id string) (*api.Campaign, error) { return newAPIClient(cfg).GetCampaign(id) } -func runCampaignsList(cfg *config.Config, params api.PaginationParams) ([]api.Campaign, error) { +func runCampaignsList(cfg *config.Config, params api.PaginationParams) ([]api.CampaignListItem, error) { client := newAPIClient(cfg) if params.Cursor != "" { campaigns, _, err := client.ListCampaigns(params) return campaigns, err } - return api.Paginate(func(cursor string) ([]api.Campaign, *api.Pagination, error) { + return api.Paginate(func(cursor string) ([]api.CampaignListItem, *api.Pagination, error) { return client.ListCampaigns(api.PaginationParams{ PerPage: params.PerPage, Cursor: cursor, @@ -53,7 +47,7 @@ var campaignsListCmd = &cobra.Command{ if isJSONOutput() { if campaigns == nil { - campaigns = []api.Campaign{} + campaigns = []api.CampaignListItem{} } return printJSON(cmd.OutOrStdout(), campaigns) } @@ -87,29 +81,13 @@ var campaignsCreateCmd = &cobra.Command{ Short: "Create a draft campaign", RunE: func(cmd *cobra.Command, args []string) error { name, _ := cmd.Flags().GetString("name") - params, err := emailMessageFieldParamsFromCmd(cmd) - if err != nil { - return err - } - - req := api.CreateCampaignRequest{Name: name} - if len(params.Set) > 0 { - req.EmailMessage = &api.EmailMessageFields{ - Subject: params.Subject, - PreviewText: params.PreviewText, - FromName: params.FromName, - FromEmail: params.FromEmail, - ReplyToEmail: params.ReplyToEmail, - LMX: params.LMX, - } - } cfg, err := loadConfig() if err != nil { return err } - resp, err := runCampaignsCreate(cfg, req) + resp, err := runCampaignsCreate(cfg, api.CreateCampaignRequest{Name: name}) if err != nil { return err } @@ -118,24 +96,7 @@ var campaignsCreateCmd = &cobra.Command{ return printJSON(cmd.OutOrStdout(), resp) } - emailMessageID := "" - if resp.EmailMessage != nil { - emailMessageID = resp.EmailMessage.EmailMessageID - } - fmt.Fprintf(cmd.OutOrStdout(), "Created. (id: %s, emailMessageId: %s)\n", resp.CampaignID, emailMessageID) - - if len(resp.Warnings) > 0 { - fmt.Fprintln(cmd.OutOrStdout()) - fmt.Fprintln(cmd.OutOrStdout(), "Warnings:") - for _, warn := range resp.Warnings { - if warn.Path != "" { - fmt.Fprintf(cmd.OutOrStdout(), " [%s] %s (%s)\n", warn.Rule, warn.Message, warn.Path) - } else { - fmt.Fprintf(cmd.OutOrStdout(), " [%s] %s\n", warn.Rule, warn.Message) - } - } - } - + fmt.Fprintf(cmd.OutOrStdout(), "Created. (id: %s, emailMessageId: %s, contentRevisionId: %s)\n", resp.CampaignID, deref(resp.EmailMessageID), deref(resp.EmailMessageContentRevisionID)) return nil }, } @@ -176,7 +137,6 @@ func init() { campaignsCmd.AddCommand(campaignsGetCmd) campaignsCreateCmd.Flags().StringP("name", "n", "", "Campaign name (required)") - addEmailMessageFieldFlags(campaignsCreateCmd) campaignsCreateCmd.MarkFlagRequired("name") campaignsCmd.AddCommand(campaignsCreateCmd) diff --git a/cmd/campaigns_create_test.go b/cmd/campaigns_create_test.go index 20dc3b5..51ccd50 100644 --- a/cmd/campaigns_create_test.go +++ b/cmd/campaigns_create_test.go @@ -15,18 +15,8 @@ func TestRunCampaignsCreate(t *testing.T) { "status": "Draft", "createdAt": "2026-04-20T10:00:00Z", "updatedAt": "2026-04-20T10:00:00Z", - "emailMessage": { - "emailMessageId": "em_new", - "campaignId": "cmp_new", - "subject": "Hello", - "previewText": "", - "fromName": "", - "fromEmail": "", - "replyToEmail": "", - "lmx": "", - "contentRevisionId": "rev_1", - "updatedAt": "2026-04-20T10:00:00Z" - } + "emailMessageId": "em_new", + "emailMessageContentRevisionId": "rev_1" }` t.Run("returns response on success", func(t *testing.T) { @@ -38,8 +28,11 @@ func TestRunCampaignsCreate(t *testing.T) { if resp.CampaignID != "cmp_new" { t.Errorf("CampaignID = %q, want cmp_new", resp.CampaignID) } - if resp.EmailMessage == nil || resp.EmailMessage.EmailMessageID != "em_new" { - t.Errorf("EmailMessage = %v, want em_new", resp.EmailMessage) + if deref(resp.EmailMessageID) != "em_new" { + t.Errorf("EmailMessageID = %q, want em_new", deref(resp.EmailMessageID)) + } + if deref(resp.EmailMessageContentRevisionID) != "rev_1" { + t.Errorf("EmailMessageContentRevisionID = %q, want rev_1", deref(resp.EmailMessageContentRevisionID)) } }) @@ -51,23 +44,3 @@ func TestRunCampaignsCreate(t *testing.T) { } }) } - -func TestFromEmailUsername(t *testing.T) { - tests := []struct { - in string - want string - }{ - {"hello", "hello"}, - {"hello@acme.com", "hello"}, - {"hello@", "hello"}, - {"", ""}, - {"@acme.com", ""}, - } - for _, tt := range tests { - t.Run(tt.in, func(t *testing.T) { - if got := fromEmailUsername(tt.in); got != tt.want { - t.Errorf("fromEmailUsername(%q) = %q, want %q", tt.in, got, tt.want) - } - }) - } -} diff --git a/cmd/email_messages.go b/cmd/email_messages.go index 02e35f2..e26832f 100644 --- a/cmd/email_messages.go +++ b/cmd/email_messages.go @@ -3,12 +3,18 @@ package cmd import ( "fmt" "os" + "strings" "github.com/loops-so/cli/internal/api" "github.com/loops-so/cli/internal/config" "github.com/spf13/cobra" ) +func fromEmailUsername(s string) string { + before, _, _ := strings.Cut(s, "@") + return before +} + // emailMessageFieldParams holds the six content fields shared by // `campaigns create` and `email-messages update`. Set records which fields the // user explicitly provided (keyed by JSON field name) so partial updates can diff --git a/cmd/email_messages_update_test.go b/cmd/email_messages_update_test.go index a162c64..cbce4f6 100644 --- a/cmd/email_messages_update_test.go +++ b/cmd/email_messages_update_test.go @@ -57,6 +57,26 @@ func TestRunEmailMessagesUpdate(t *testing.T) { }) } +func TestFromEmailUsername(t *testing.T) { + tests := []struct { + in string + want string + }{ + {"hello", "hello"}, + {"hello@acme.com", "hello"}, + {"hello@", "hello"}, + {"", ""}, + {"@acme.com", ""}, + } + for _, tt := range tests { + t.Run(tt.in, func(t *testing.T) { + if got := fromEmailUsername(tt.in); got != tt.want { + t.Errorf("fromEmailUsername(%q) = %q, want %q", tt.in, got, tt.want) + } + }) + } +} + func TestEmailMessageFieldParamsFromCmd(t *testing.T) { t.Run("unset flags are absent from Set", func(t *testing.T) { cmd := &cobra.Command{} diff --git a/internal/api/campaigns.go b/internal/api/campaigns.go index 19f101d..e5e5f42 100644 --- a/internal/api/campaigns.go +++ b/internal/api/campaigns.go @@ -9,6 +9,15 @@ import ( ) type Campaign struct { + CampaignID string `json:"campaignId"` + EmailMessageID *string `json:"emailMessageId"` + Name string `json:"name"` + Status string `json:"status"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +type CampaignListItem struct { CampaignID string `json:"campaignId"` EmailMessageID *string `json:"emailMessageId"` Name string `json:"name"` @@ -35,18 +44,12 @@ type EmailMessageFields struct { } type CreateCampaignRequest struct { - Name string `json:"name"` - EmailMessage *EmailMessageFields `json:"emailMessage,omitempty"` + Name string `json:"name"` } type CampaignCreateResponse struct { - CampaignID string `json:"campaignId"` - Name string `json:"name"` - Status string `json:"status"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` - EmailMessage *EmailMessage `json:"emailMessage,omitempty"` - Warnings []LmxWarning `json:"warnings,omitempty"` + Campaign + EmailMessageContentRevisionID *string `json:"emailMessageContentRevisionId"` } func (c *Client) CreateCampaign(req CreateCampaignRequest) (*CampaignCreateResponse, error) { @@ -102,7 +105,7 @@ func (c *Client) GetCampaign(id string) (*Campaign, error) { return &result, nil } -func (c *Client) ListCampaigns(params PaginationParams) ([]Campaign, *Pagination, error) { +func (c *Client) ListCampaigns(params PaginationParams) ([]CampaignListItem, *Pagination, error) { q := url.Values{} if params.PerPage != "" { q.Set("perPage", params.PerPage) @@ -132,8 +135,8 @@ func (c *Client) ListCampaigns(params PaginationParams) ([]Campaign, *Pagination } var result struct { - Pagination Pagination `json:"pagination"` - Data []Campaign `json:"data"` + Pagination Pagination `json:"pagination"` + Data []CampaignListItem `json:"data"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, nil, fmt.Errorf("failed to decode response: %w", err) diff --git a/internal/api/campaigns_test.go b/internal/api/campaigns_test.go index 470885e..fd699ab 100644 --- a/internal/api/campaigns_test.go +++ b/internal/api/campaigns_test.go @@ -49,21 +49,8 @@ const createCampaignResponse = `{ "status": "Draft", "createdAt": "2026-04-20T10:00:00Z", "updatedAt": "2026-04-20T10:00:00Z", - "emailMessage": { - "emailMessageId": "em_new", - "campaignId": "cmp_new", - "subject": "Hello", - "previewText": "Preview", - "fromName": "Acme", - "fromEmail": "hello", - "replyToEmail": "support@acme.com", - "lmx": "Hi", - "contentRevisionId": "rev_1", - "updatedAt": "2026-04-20T10:00:00Z" - }, - "warnings": [ - {"rule":"unknown_attr","severity":"warning","message":" has unknown attribute \"foo\"","path":"body.0"} - ] + "emailMessageId": "em_new", + "emailMessageContentRevisionId": "rev_1" }` func TestCreateCampaign(t *testing.T) { @@ -85,12 +72,6 @@ func TestCreateCampaign(t *testing.T) { body: `{"success":false,"message":"name is required"}`, wantAPIErr: &APIError{StatusCode: http.StatusBadRequest, Message: "name is required"}, }, - { - name: "lmx compile failure", - statusCode: http.StatusUnprocessableEntity, - body: `{"success":false,"message":"LMX failed to compile"}`, - wantAPIErr: &APIError{StatusCode: http.StatusUnprocessableEntity, Message: "LMX failed to compile"}, - }, { name: "invalid json", statusCode: http.StatusCreated, @@ -140,75 +121,36 @@ func TestCreateCampaign(t *testing.T) { if resp.CampaignID != "cmp_new" { t.Errorf("CampaignID = %q, want cmp_new", resp.CampaignID) } - if resp.EmailMessage == nil || resp.EmailMessage.EmailMessageID != "em_new" { - t.Errorf("EmailMessage.EmailMessageID = %v, want em_new", resp.EmailMessage) + if resp.EmailMessageID == nil || *resp.EmailMessageID != "em_new" { + t.Errorf("EmailMessageID = %v, want em_new", resp.EmailMessageID) } - if len(resp.Warnings) != 1 || resp.Warnings[0].Rule != "unknown_attr" { - t.Errorf("Warnings = %v, want [unknown_attr]", resp.Warnings) + if resp.EmailMessageContentRevisionID == nil || *resp.EmailMessageContentRevisionID != "rev_1" { + t.Errorf("EmailMessageContentRevisionID = %v, want rev_1", resp.EmailMessageContentRevisionID) } }) } } func TestCreateCampaign_RequestBody(t *testing.T) { - tests := []struct { - name string - req CreateCampaignRequest - wantName string - wantEmail bool - }{ - { - name: "name only", - req: CreateCampaignRequest{Name: "Spring"}, - wantName: "Spring", - }, - { - name: "with email message", - req: CreateCampaignRequest{ - Name: "Spring", - EmailMessage: &EmailMessageFields{ - Subject: "Hello", - LMX: "Hi", - }, - }, - wantName: "Spring", - wantEmail: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var body map[string]any - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - b, _ := io.ReadAll(r.Body) - json.Unmarshal(b, &body) - w.WriteHeader(http.StatusCreated) - w.Write([]byte(createCampaignResponse)) - })) - defer server.Close() + var body map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, _ := io.ReadAll(r.Body) + json.Unmarshal(b, &body) + w.WriteHeader(http.StatusCreated) + w.Write([]byte(createCampaignResponse)) + })) + defer server.Close() - client := NewClient(server.URL, "test-key", false) - if _, err := client.CreateCampaign(tt.req); err != nil { - t.Fatalf("unexpected error: %v", err) - } + client := NewClient(server.URL, "test-key", false) + if _, err := client.CreateCampaign(CreateCampaignRequest{Name: "Spring"}); err != nil { + t.Fatalf("unexpected error: %v", err) + } - if body["name"] != tt.wantName { - t.Errorf("name = %v, want %q", body["name"], tt.wantName) - } - _, hasEmail := body["emailMessage"] - if hasEmail != tt.wantEmail { - t.Errorf("emailMessage present = %v, want %v", hasEmail, tt.wantEmail) - } - if tt.wantEmail { - em, _ := body["emailMessage"].(map[string]any) - if em["subject"] != "Hello" { - t.Errorf("emailMessage.subject = %v, want Hello", em["subject"]) - } - if em["lmx"] != "Hi" { - t.Errorf("emailMessage.lmx = %v", em["lmx"]) - } - } - }) + if body["name"] != "Spring" { + t.Errorf("name = %v, want Spring", body["name"]) + } + if _, hasEmail := body["emailMessage"]; hasEmail { + t.Errorf("emailMessage should not be present in request body, got %v", body["emailMessage"]) } }