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
2 changes: 1 addition & 1 deletion cmd/sumup/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func main() {
}

if err := cliApp.Run(context.Background(), os.Args); err != nil {
message.Error("%v", err)
message.Error(os.Stderr, "%v", err)
os.Exit(1)
}
}
6 changes: 5 additions & 1 deletion internal/app/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,11 @@ func GetAppContext(cmd *cli.Command) (*Context, error) {
// falling back to the stored context if the flag is not set.
func GetMerchantCode(cmd *cli.Command, flagName string) (string, error) {
if cmd.IsSet(flagName) {
return cmd.String(flagName), nil
merchantCode := strings.TrimSpace(cmd.String(flagName))
if merchantCode == "" {
return "", errors.New("merchant code is required. Provide --merchant-code flag or set context with 'sumup context set'")
}
return merchantCode, nil
}

appCtx, err := GetAppContext(cmd)
Expand Down
11 changes: 11 additions & 0 deletions internal/app/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ func TestGetMerchantCode(t *testing.T) {
assert.Empty(t, got)
assert.ErrorContains(t, err, "merchant code is required")
})

t.Run("rejects explicitly empty merchant-code flag", func(t *testing.T) {
tempDir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", tempDir)
t.Setenv("HOME", tempDir)

got, err := runGetMerchantCode(t, []string{"sumup", "--merchant-code", ""}, nil)
require.Error(t, err)
assert.Empty(t, got)
assert.ErrorContains(t, err, "merchant code is required")
})
}

func TestNewContext(t *testing.T) {
Expand Down
8 changes: 4 additions & 4 deletions internal/commands/checkouts/checkouts.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ func createCheckout(ctx context.Context, cmd *cli.Command) error {
return display.PrintJSON(appCtx.Output, checkout)
}

message.Success("Checkout created")
message.Success(appCtx.Output, "Checkout created")
details := make([]attribute.KeyValue, 0, 5)
if checkout.ID != nil {
details = append(details, attribute.ID(*checkout.ID))
Expand Down Expand Up @@ -275,7 +275,7 @@ func deactivateCheckout(ctx context.Context, cmd *cli.Command) error {
return display.PrintJSON(appCtx.Output, checkout)
}

message.Success("Checkout deactivated")
message.Success(appCtx.Output, "Checkout deactivated")
details := make([]attribute.KeyValue, 0, 4)
if checkout.ID != nil {
details = append(details, attribute.ID(*checkout.ID))
Expand Down Expand Up @@ -405,13 +405,13 @@ func processCheckout(ctx context.Context, cmd *cli.Command) error {
}

if response.CheckoutSuccess != nil {
message.Success("Checkout processed")
message.Success(appCtx.Output, "Checkout processed")
renderCheckout(appCtx, response.CheckoutSuccess)
return nil
}

if response.CheckoutAccepted != nil {
message.Success("Checkout accepted")
message.Success(appCtx.Output, "Checkout accepted")
if response.CheckoutAccepted.NextStep != nil {
display.DataList(appCtx.Output, []attribute.KeyValue{
attribute.OptionalString("Method", response.CheckoutAccepted.NextStep.Method),
Expand Down
63 changes: 47 additions & 16 deletions internal/commands/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func NewCommand() *cli.Command {
}

type searchResultMsg struct {
requestID int
memberships []sumup.Membership
err error
}
Expand Down Expand Up @@ -86,6 +87,10 @@ type model struct {
lastSearchQuery string
// Whether a search is pending (debouncing)
searchPending bool
// Monotonic counter for searches so stale async responses can be ignored.
nextSearchID int
// Most recent in-flight search request.
activeSearchID int
}

func (m model) Init() tea.Cmd {
Expand All @@ -99,6 +104,8 @@ func (m *model) clearSearch() {
m.displayed = m.currentLevel.memberships
m.lastSearchQuery = ""
m.cursor = 0
m.loading = false
m.activeSearchID = 0
}

// pushLevel saves current level to stack and sets a new current level
Expand All @@ -119,6 +126,8 @@ func (m *model) popLevel() {
m.currentLevel = previousLevel
m.displayed = previousLevel.memberships
m.cursor = 0
m.loading = false
m.activeSearchID = 0
}

// drillDownIntoOrg navigates into an organization to view its child merchants
Expand All @@ -132,7 +141,7 @@ func (m *model) drillDownIntoOrg(orgID, orgName string) tea.Cmd {
}
m.pushLevel(newLevel)
m.loading = true
return m.searchMemberships("", orgID, parentType)
return m.startMembershipSearch("", orgID, parentType)
}

// debounce returns a command that waits for the debounce delay before sending a searchDebounceMsg
Expand All @@ -143,7 +152,15 @@ func debounce() tea.Cmd {
}

// searchMemberships performs an API call to search for memberships by name
func (m model) searchMemberships(query string, parentID string, parentType sumup.ResourceType) tea.Cmd {
func (m *model) startMembershipSearch(query string, parentID string, parentType sumup.ResourceType) tea.Cmd {
m.nextSearchID++
requestID := m.nextSearchID
m.activeSearchID = requestID

return m.searchMemberships(requestID, query, parentID, parentType)
}

func (m model) searchMemberships(requestID int, query string, parentID string, parentType sumup.ResourceType) tea.Cmd {
return func() tea.Msg {
status := sumup.MembershipStatusAccepted
params := sumup.MembershipsListParams{
Expand All @@ -164,10 +181,10 @@ func (m model) searchMemberships(query string, parentID string, parentType sumup

response, err := m.client.Memberships.List(m.ctx, params)
if err != nil {
return searchResultMsg{err: err}
return searchResultMsg{requestID: requestID, err: err}
}

return searchResultMsg{memberships: response.Items}
return searchResultMsg{requestID: requestID, memberships: response.Items}
}
}

Expand All @@ -176,6 +193,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

switch msg := msg.(type) {
case searchResultMsg:
if msg.requestID != m.activeSearchID {
return m, nil
}

m.loading = false
if msg.err != nil {
m.err = msg.err
Expand All @@ -199,7 +220,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
m.lastSearchQuery = query
m.loading = true
return m, m.searchMemberships(query, m.currentLevel.parentID, m.currentLevel.parentType)
return m, m.startMembershipSearch(query, m.currentLevel.parentID, m.currentLevel.parentType)

case tea.KeyPressMsg:
switch msg.String() {
Expand Down Expand Up @@ -373,7 +394,7 @@ func setContext(ctx context.Context, cmd *cli.Command) error {
return err
}

message.Notify("Fetching your sumup...")
message.Notify(appCtx.Output, "Fetching your sumup...")

status := sumup.MembershipStatusAccepted
params := sumup.MembershipsListParams{
Expand All @@ -386,7 +407,7 @@ func setContext(ctx context.Context, cmd *cli.Command) error {
}

if len(response.Items) == 0 {
message.Warn("No memberships found.")
message.Warn(appCtx.Output, "No memberships found.")
return nil
}

Expand All @@ -411,12 +432,12 @@ func setContext(ctx context.Context, cmd *cli.Command) error {

finalModel := result.(model)
if finalModel.selected == nil {
message.Warn("No merchant selected.")
message.Warn(appCtx.Output, "No merchant selected.")
return nil
}

if finalModel.selected.Resource.Type == "organization" {
message.Warn("Please select a merchant, not an organization.")
message.Warn(appCtx.Output, "Please select a merchant, not an organization.")
return nil
}

Expand All @@ -430,31 +451,41 @@ func setContext(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("save merchant context: %w", err)
}

message.Success("Merchant context set to: %s (%s)", finalModel.selected.Resource.Name, merchantCode)
message.Success(appCtx.Output, "Merchant context set to: %s (%s)", finalModel.selected.Resource.Name, merchantCode)
return nil
}

func getContext(_ context.Context, _ *cli.Command) error {
func getContext(_ context.Context, cmd *cli.Command) error {
appCtx, err := app.GetAppContext(cmd)
if err != nil {
return err
}

merchantCode, err := config.GetCurrentMerchantCode()
if err != nil {
return fmt.Errorf("get merchant context: %w", err)
}

if merchantCode == "" {
message.Notify("No merchant context set.")
message.Notify("Use 'sumup context set' to set a merchant context.")
message.Notify(appCtx.Output, "No merchant context set.")
message.Notify(appCtx.Output, "Use 'sumup context set' to set a merchant context.")
return nil
}

message.Notify("Current merchant context: %s", merchantCode)
message.Notify(appCtx.Output, "Current merchant context: %s", merchantCode)
return nil
}

func unsetContext(_ context.Context, _ *cli.Command) error {
func unsetContext(_ context.Context, cmd *cli.Command) error {
appCtx, err := app.GetAppContext(cmd)
if err != nil {
return err
}

if err := config.SetCurrentMerchantCode(""); err != nil {
return fmt.Errorf("unset merchant context: %w", err)
}

message.Success("Merchant context unset.")
message.Success(appCtx.Output, "Merchant context unset.")
return nil
}
58 changes: 58 additions & 0 deletions internal/commands/context/context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package context

import (
"testing"

sumup "github.com/sumup/sumup-go"
)

func TestModelUpdateIgnoresStaleSearchResult(t *testing.T) {
currentItems := []sumup.Membership{{Resource: sumup.MembershipResource{ID: "current"}}}
m := model{
currentLevel: navigationLevel{memberships: currentItems},
displayed: currentItems,
loading: true,
activeSearchID: 2,
}

updated, cmd := m.Update(searchResultMsg{
requestID: 1,
memberships: []sumup.Membership{{Resource: sumup.MembershipResource{ID: "stale"}}},
})
if cmd != nil {
t.Fatalf("Update() cmd = %v, want nil", cmd)
}

got := updated.(model)
if !got.loading {
t.Fatal("Update() loading = false, want true for ignored stale result")
}
if got.displayed[0].Resource.ID != "current" {
t.Fatalf("Update() displayed = %q, want current result to remain", got.displayed[0].Resource.ID)
}
}

func TestModelUpdateAppliesActiveSearchResult(t *testing.T) {
m := model{
currentLevel: navigationLevel{},
displayed: nil,
loading: true,
activeSearchID: 3,
}

updated, cmd := m.Update(searchResultMsg{
requestID: 3,
memberships: []sumup.Membership{{Resource: sumup.MembershipResource{ID: "fresh"}}},
})
if cmd != nil {
t.Fatalf("Update() cmd = %v, want nil", cmd)
}

got := updated.(model)
if got.loading {
t.Fatal("Update() loading = true, want false")
}
if len(got.displayed) != 1 || got.displayed[0].Resource.ID != "fresh" {
t.Fatalf("Update() displayed = %+v, want active search result", got.displayed)
}
}
6 changes: 3 additions & 3 deletions internal/commands/customers/customers.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ func createCustomer(ctx context.Context, cmd *cli.Command) error {
return display.PrintJSON(appCtx.Output, customer)
}

message.Success("Customer created")
message.Success(appCtx.Output, "Customer created")
renderCustomer(appCtx.Output, customer)
return nil
}
Expand Down Expand Up @@ -201,7 +201,7 @@ func updateCustomer(ctx context.Context, cmd *cli.Command) error {
return display.PrintJSON(appCtx.Output, customer)
}

message.Success("Customer updated")
message.Success(appCtx.Output, "Customer updated")
renderCustomer(appCtx.Output, customer)
return nil
}
Expand Down Expand Up @@ -264,7 +264,7 @@ func deactivatePaymentInstrument(ctx context.Context, cmd *cli.Command) error {
return display.PrintJSON(appCtx.Output, map[string]string{"status": "deactivated"})
}

message.Success("Payment instrument deactivated")
message.Success(appCtx.Output, "Payment instrument deactivated")
return nil
}

Expand Down
8 changes: 4 additions & 4 deletions internal/commands/members/members.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ func createMember(ctx context.Context, cmd *cli.Command) error {
return display.PrintJSON(appCtx.Output, response)
}

message.Success("Member created")
message.Success(appCtx.Output, "Member created")
display.DataList(appCtx.Output, []attribute.KeyValue{
attribute.ID(response.ID),
})
Expand Down Expand Up @@ -307,7 +307,7 @@ func inviteMember(ctx context.Context, cmd *cli.Command) error {
return display.PrintJSON(appCtx.Output, response)
}

message.Success("Member invited")
message.Success(appCtx.Output, "Member invited")
display.DataList(appCtx.Output, []attribute.KeyValue{
attribute.ID(response.ID),
})
Expand Down Expand Up @@ -400,7 +400,7 @@ func updateMember(ctx context.Context, cmd *cli.Command) error {
return display.PrintJSON(appCtx.Output, member)
}

message.Success("Member updated")
message.Success(appCtx.Output, "Member updated")
renderMember(appCtx.Output, member)
return nil
}
Expand All @@ -424,7 +424,7 @@ func deleteMember(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("delete member: %w", err)
}

message.Success("Member deleted")
message.Success(appCtx.Output, "Member deleted")
return nil
}

Expand Down
Loading