From 9bcd917b422db610a4096ec81b46610266eb7069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Thu, 26 Mar 2026 13:03:42 +0100 Subject: [PATCH 1/2] feat(cli): expand sdk command coverage --- internal/commands/checkouts/checkouts.go | 272 ++++++++++++ internal/commands/commands.go | 2 + internal/commands/customers/customers.go | 97 +++++ internal/commands/readers/readers.go | 144 +++++++ internal/commands/subaccounts/subaccounts.go | 396 ++++++++++++++++++ .../commands/transactions/transactions.go | 91 +++- 6 files changed, 995 insertions(+), 7 deletions(-) create mode 100644 internal/commands/subaccounts/subaccounts.go diff --git a/internal/commands/checkouts/checkouts.go b/internal/commands/checkouts/checkouts.go index 0726432..7388c07 100644 --- a/internal/commands/checkouts/checkouts.go +++ b/internal/commands/checkouts/checkouts.go @@ -9,6 +9,7 @@ import ( "github.com/urfave/cli/v3" sumup "github.com/sumup/sumup-go" + "github.com/sumup/sumup-go/datetime" "github.com/sumup/sumup-cli/internal/app" "github.com/sumup/sumup-cli/internal/commands/util" @@ -90,6 +91,60 @@ func NewCommand() *cli.Command { Action: deactivateCheckout, ArgsUsage: "", }, + { + Name: "get", + Usage: "Get a checkout by ID.", + Action: getCheckout, + ArgsUsage: "", + }, + { + Name: "payment-methods", + Usage: "List available payment methods for a merchant.", + Action: listPaymentMethods, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "merchant-code", + Usage: "Merchant code that owns the checkout. Falls back to context.", + Sources: cli.EnvVars("SUMUP_MERCHANT_CODE"), + }, + &cli.Float64Flag{ + Name: "amount", + Usage: "Optional amount filter.", + }, + &cli.StringFlag{ + Name: "currency", + Usage: fmt.Sprintf("Optional currency filter. Supported: %s", strings.Join(currency.Supported(), ", ")), + }, + }, + }, + { + Name: "process", + Usage: "Process a checkout.", + Action: processCheckout, + ArgsUsage: "", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "payment-type", + Usage: "Payment type to use when processing the checkout.", + Required: true, + }, + &cli.StringFlag{Name: "customer-id", Usage: "Customer ID for tokenized payments."}, + &cli.StringFlag{Name: "token", Usage: "Saved payment instrument token."}, + &cli.IntFlag{Name: "installments", Usage: "Installment count for supported regions."}, + &cli.StringFlag{Name: "first-name", Usage: "Customer first name."}, + &cli.StringFlag{Name: "last-name", Usage: "Customer last name."}, + &cli.StringFlag{Name: "email", Usage: "Customer email."}, + &cli.StringFlag{Name: "phone", Usage: "Customer phone."}, + &cli.StringFlag{Name: "tax-id", Usage: "Customer tax ID."}, + &cli.StringFlag{Name: "birth-date", Usage: "Customer birth date in YYYY-MM-DD format."}, + &cli.StringFlag{Name: "card-name", Usage: "Cardholder name."}, + &cli.StringFlag{Name: "card-number", Usage: "Card number without spaces."}, + &cli.StringFlag{Name: "card-cvv", Usage: "Card CVV."}, + &cli.StringFlag{Name: "card-expiry-month", Usage: "Card expiry month in MM format."}, + &cli.StringFlag{Name: "card-expiry-year", Usage: "Card expiry year in YY or YYYY format."}, + &cli.StringFlag{Name: "card-zip-code", Usage: "Card ZIP code when required."}, + }, + }, }, } } @@ -238,3 +293,220 @@ func deactivateCheckout(ctx context.Context, cmd *cli.Command) error { display.DataList(details) return nil } + +func getCheckout(ctx context.Context, cmd *cli.Command) error { + appCtx, err := app.GetAppContext(cmd) + if err != nil { + return err + } + checkoutID, err := util.RequireSingleArg(cmd, "checkout ID") + if err != nil { + return err + } + + checkout, err := appCtx.Client.Checkouts.Get(ctx, checkoutID) + if err != nil { + return fmt.Errorf("get checkout: %w", err) + } + + if appCtx.JSONOutput { + return display.PrintJSON(checkout) + } + + renderCheckout(appCtx, checkout) + return nil +} + +func listPaymentMethods(ctx context.Context, cmd *cli.Command) error { + appCtx, err := app.GetAppContext(cmd) + if err != nil { + return err + } + merchantCode, err := app.GetMerchantCode(cmd, "merchant-code") + if err != nil { + return err + } + + params := sumup.CheckoutsListAvailablePaymentMethodsParams{} + if cmd.IsSet("amount") { + value := cmd.Float64("amount") + params.Amount = &value + } + if value := cmd.String("currency"); value != "" { + parsedCurrency, err := currency.Parse(value) + if err != nil { + return err + } + c := string(parsedCurrency) + params.Currency = &c + } + + methods, err := appCtx.Client.Checkouts.ListAvailablePaymentMethods(ctx, merchantCode, params) + if err != nil { + return fmt.Errorf("list checkout payment methods: %w", err) + } + + if appCtx.JSONOutput { + return display.PrintJSON(methods.AvailablePaymentMethods) + } + + rows := make([][]attribute.Value, 0, len(methods.AvailablePaymentMethods)) + for _, method := range methods.AvailablePaymentMethods { + rows = append(rows, []attribute.Value{attribute.ValueOf(method.ID)}) + } + + display.RenderTable("Checkout Payment Methods", []string{"ID"}, rows) + return nil +} + +func processCheckout(ctx context.Context, cmd *cli.Command) error { + appCtx, err := app.GetAppContext(cmd) + if err != nil { + return err + } + checkoutID, err := util.RequireSingleArg(cmd, "checkout ID") + if err != nil { + return err + } + + body := sumup.CheckoutsProcessParams{ + PaymentType: sumup.ProcessCheckoutPaymentType(cmd.String("payment-type")), + } + if customerID := cmd.String("customer-id"); customerID != "" { + body.CustomerID = &customerID + } + if token := cmd.String("token"); token != "" { + body.Token = &token + } + if cmd.IsSet("installments") { + value := cmd.Int("installments") + body.Installments = &value + } + if details, changedCount, err := checkoutPersonalDetailsFromFlags(cmd); err != nil { + return err + } else if changedCount > 0 { + body.PersonalDetails = details + } + if card, err := checkoutCardFromFlags(cmd); err != nil { + return err + } else if card != nil { + body.Card = card + } + + response, err := appCtx.Client.Checkouts.Process(ctx, checkoutID, body) + if err != nil { + return fmt.Errorf("process checkout: %w", err) + } + + if appCtx.JSONOutput { + return display.PrintJSON(response) + } + + if response.CheckoutSuccess != nil { + message.Success("Checkout processed") + renderCheckout(appCtx, response.CheckoutSuccess) + return nil + } + + if response.CheckoutAccepted != nil { + message.Success("Checkout accepted") + if response.CheckoutAccepted.NextStep != nil { + display.DataList([]attribute.KeyValue{ + attribute.OptionalString("Method", response.CheckoutAccepted.NextStep.Method), + attribute.OptionalString("URL", response.CheckoutAccepted.NextStep.URL), + attribute.OptionalString("Redirect URL", response.CheckoutAccepted.NextStep.RedirectURL), + }) + } + } + return nil +} + +func renderCheckout(appCtx *app.Context, checkout *sumup.CheckoutSuccess) { + if checkout == nil { + return + } + + details := []attribute.KeyValue{} + if checkout.ID != nil { + details = append(details, attribute.ID(*checkout.ID)) + } + details = append(details, + attribute.Attribute("Reference", attribute.Styled(util.StringOrDefault(checkout.CheckoutReference, "-"))), + attribute.Attribute("Amount", attribute.Styled(currency.FormatPointers(checkout.Amount, checkout.Currency))), + attribute.OptionalString("Merchant", checkout.MerchantCode), + attribute.OptionalString("Merchant Name", checkout.MerchantName), + attribute.OptionalString("Description", checkout.Description), + attribute.OptionalString("Status", enumString(checkout.Status)), + attribute.OptionalString("Transaction ID", checkout.TransactionID), + attribute.OptionalString("Transaction Code", checkout.TransactionCode), + attribute.Attribute("Created At", attribute.Styled(util.TimeOrDash(appCtx, checkout.Date))), + ) + display.DataList(details) +} + +func checkoutPersonalDetailsFromFlags(cmd *cli.Command) (*sumup.PersonalDetails, int, error) { + details := &sumup.PersonalDetails{} + changedCount := 0 + + if value := cmd.String("first-name"); value != "" { + details.FirstName = &value + changedCount++ + } + if value := cmd.String("last-name"); value != "" { + details.LastName = &value + changedCount++ + } + if value := cmd.String("email"); value != "" { + details.Email = &value + changedCount++ + } + if value := cmd.String("phone"); value != "" { + details.Phone = &value + changedCount++ + } + if value := cmd.String("tax-id"); value != "" { + details.TaxID = &value + changedCount++ + } + if value := cmd.String("birth-date"); value != "" { + parsedDate, err := time.Parse(time.DateOnly, value) + if err != nil { + return nil, 0, fmt.Errorf("invalid birth date %q: %w", value, err) + } + date := datetime.Date{Time: parsedDate} + details.BirthDate = &date + changedCount++ + } + + if changedCount == 0 { + return nil, 0, nil + } + return details, changedCount, nil +} + +func checkoutCardFromFlags(cmd *cli.Command) (*sumup.Card, error) { + number := cmd.String("card-number") + if number == "" { + return nil, nil + } + + card := &sumup.Card{ + Number: number, + Name: cmd.String("card-name"), + Cvv: cmd.String("card-cvv"), + ExpiryMonth: sumup.CardExpiryMonth(cmd.String("card-expiry-month")), + ExpiryYear: cmd.String("card-expiry-year"), + } + if zipCode := cmd.String("card-zip-code"); zipCode != "" { + card.ZipCode = &zipCode + } + return card, nil +} + +func enumString[T ~string](value *T) *string { + if value == nil { + return nil + } + text := string(*value) + return &text +} diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 766cc9c..a4802f9 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -13,6 +13,7 @@ import ( "github.com/sumup/sumup-cli/internal/commands/readers" "github.com/sumup/sumup-cli/internal/commands/receipts" "github.com/sumup/sumup-cli/internal/commands/roles" + "github.com/sumup/sumup-cli/internal/commands/subaccounts" "github.com/sumup/sumup-cli/internal/commands/transactions" "github.com/sumup/sumup-cli/internal/commands/version" ) @@ -30,6 +31,7 @@ func All() []*cli.Command { readers.NewCommand(), receipts.NewCommand(), roles.NewCommand(), + subaccounts.NewCommand(), transactions.NewCommand(), version.NewCommand(), } diff --git a/internal/commands/customers/customers.go b/internal/commands/customers/customers.go index fc54fe6..af21381 100644 --- a/internal/commands/customers/customers.go +++ b/internal/commands/customers/customers.go @@ -42,6 +42,24 @@ func NewCommand() *cli.Command { ArgsUsage: "", Flags: customerDetailsFlags(), }, + { + Name: "payment-instruments", + Usage: "Manage stored payment instruments for a customer.", + Commands: []*cli.Command{ + { + Name: "list", + Usage: "List stored payment instruments for a customer.", + Action: listPaymentInstruments, + ArgsUsage: "", + }, + { + Name: "deactivate", + Usage: "Deactivate a stored payment instrument.", + Action: deactivatePaymentInstrument, + ArgsUsage: " ", + }, + }, + }, }, } } @@ -187,6 +205,67 @@ func updateCustomer(ctx context.Context, cmd *cli.Command) error { return nil } +func listPaymentInstruments(ctx context.Context, cmd *cli.Command) error { + appCtx, err := app.GetAppContext(cmd) + if err != nil { + return err + } + if cmd.Args().Len() != 1 { + return fmt.Errorf("expected exactly 1 argument: customer ID") + } + + customerID := cmd.Args().Get(0) + instruments, err := appCtx.Client.Customers.ListPaymentInstruments(ctx, customerID) + if err != nil { + return fmt.Errorf("list payment instruments: %w", err) + } + + if appCtx.JSONOutput { + return display.PrintJSON(instruments) + } + + rows := make([][]attribute.Value, 0, len(*instruments)) + for _, instrument := range *instruments { + rows = append(rows, []attribute.Value{ + attribute.OptionalStringValue(instrument.Token), + attribute.OptionalValue(instrument.Type), + attribute.ValueOf(paymentInstrumentCardLabel(instrument.Card)), + attribute.ValueOf(util.BoolLabel(instrument.Active)), + attribute.ValueOf(util.TimeOrDash(appCtx, instrument.CreatedAt)), + }) + } + + display.RenderTable( + "Payment Instruments", + []string{"Token", "Type", "Card", "Active", "Created At"}, + rows, + ) + return nil +} + +func deactivatePaymentInstrument(ctx context.Context, cmd *cli.Command) error { + appCtx, err := app.GetAppContext(cmd) + if err != nil { + return err + } + if cmd.Args().Len() != 2 { + return fmt.Errorf("expected exactly 2 arguments: customer ID and token") + } + + customerID := cmd.Args().Get(0) + token := cmd.Args().Get(1) + if err := appCtx.Client.Customers.DeactivatePaymentInstrument(ctx, customerID, token); err != nil { + return fmt.Errorf("deactivate payment instrument: %w", err) + } + + if appCtx.JSONOutput { + return display.PrintJSON(map[string]string{"status": "deactivated"}) + } + + message.Success("Payment instrument deactivated") + return nil +} + func customerDetailsFromFlags(cmd *cli.Command) (*sumup.PersonalDetails, int, error) { details := &sumup.PersonalDetails{} changedCount := 0 @@ -330,3 +409,21 @@ func formatAddress(address *sumup.AddressLegacy) string { } return strings.Join(parts, ", ") } + +func paymentInstrumentCardLabel(card *sumup.PaymentInstrumentResponseCard) string { + if card == nil { + return "-" + } + + parts := make([]string, 0, 2) + if card.Type != nil && *card.Type != "" { + parts = append(parts, string(*card.Type)) + } + if card.Last4Digits != nil && *card.Last4Digits != "" { + parts = append(parts, fmt.Sprintf("(****%s)", *card.Last4Digits)) + } + if len(parts) == 0 { + return "-" + } + return strings.Join(parts, " ") +} diff --git a/internal/commands/readers/readers.go b/internal/commands/readers/readers.go index 8f5b186..1a9a320 100644 --- a/internal/commands/readers/readers.go +++ b/internal/commands/readers/readers.go @@ -6,6 +6,7 @@ import ( "fmt" "math" "strings" + "time" "github.com/urfave/cli/v3" @@ -89,6 +90,57 @@ func NewCommand() *cli.Command { }, }, }, + { + Name: "get", + Usage: "Get a paired reader.", + Action: getReader, + ArgsUsage: "", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "merchant-code", + Usage: "Merchant code that owns the reader.", + Sources: cli.EnvVars("SUMUP_MERCHANT_CODE"), + Required: true, + }, + &cli.StringFlag{ + Name: "if-modified-since", + Usage: "Optional If-Modified-Since query value.", + }, + }, + }, + { + Name: "update", + Usage: "Update a paired reader.", + Action: updateReader, + ArgsUsage: "", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "merchant-code", + Usage: "Merchant code that owns the reader.", + Sources: cli.EnvVars("SUMUP_MERCHANT_CODE"), + Required: true, + }, + &cli.StringFlag{ + Name: "name", + Usage: "Updated reader name.", + Required: true, + }, + }, + }, + { + Name: "terminate", + Usage: "Terminate the current reader checkout.", + Action: terminateCheckout, + ArgsUsage: "", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "merchant-code", + Usage: "Merchant code that owns the reader.", + Sources: cli.EnvVars("SUMUP_MERCHANT_CODE"), + Required: true, + }, + }, + }, { Name: "checkout", Usage: "Trigger a checkout on a specific reader device.", @@ -371,6 +423,98 @@ func readerStatus(ctx context.Context, cmd *cli.Command) error { return nil } +func getReader(ctx context.Context, cmd *cli.Command) error { + appCtx, err := app.GetAppContext(cmd) + if err != nil { + return err + } + readerID, err := util.RequireSingleArg(cmd, "reader ID") + if err != nil { + return err + } + + params := sumup.ReadersGetParams{} + if value := cmd.String("if-modified-since"); value != "" { + params.IfModifiedSince = &value + } + + reader, err := appCtx.Client.Readers.Get(ctx, cmd.String("merchant-code"), sumup.ReaderID(readerID), params) + if err != nil { + return fmt.Errorf("get reader: %w", err) + } + + if appCtx.JSONOutput { + return display.PrintJSON(reader) + } + + renderReader(reader) + return nil +} + +func updateReader(ctx context.Context, cmd *cli.Command) error { + appCtx, err := app.GetAppContext(cmd) + if err != nil { + return err + } + readerID, err := util.RequireSingleArg(cmd, "reader ID") + if err != nil { + return err + } + + name := sumup.ReaderName(cmd.String("name")) + body := sumup.ReadersUpdateParams{Name: &name} + reader, err := appCtx.Client.Readers.Update(ctx, cmd.String("merchant-code"), sumup.ReaderID(readerID), body) + if err != nil { + return fmt.Errorf("update reader: %w", err) + } + + if appCtx.JSONOutput { + return display.PrintJSON(reader) + } + + message.Success("Reader updated") + renderReader(reader) + return nil +} + +func terminateCheckout(ctx context.Context, cmd *cli.Command) error { + appCtx, err := app.GetAppContext(cmd) + if err != nil { + return err + } + readerID, err := util.RequireSingleArg(cmd, "reader ID") + if err != nil { + return err + } + + if err := appCtx.Client.Readers.TerminateCheckout(ctx, cmd.String("merchant-code"), readerID); err != nil { + return fmt.Errorf("terminate reader checkout: %w", err) + } + + if appCtx.JSONOutput { + return display.PrintJSON(map[string]string{"status": "termination_requested"}) + } + + message.Success("Reader checkout termination requested") + return nil +} + +func renderReader(reader *sumup.Reader) { + if reader == nil { + return + } + + display.DataList([]attribute.KeyValue{ + attribute.ID(string(reader.ID)), + attribute.Attribute("Name", attribute.Styled(string(reader.Name))), + attribute.Attribute("Status", attribute.Styled(string(reader.Status))), + attribute.Attribute("Model", attribute.Styled(string(reader.Device.Model))), + attribute.Attribute("Identifier", attribute.Styled(reader.Device.Identifier)), + attribute.Attribute("Updated At", attribute.Styled(reader.UpdatedAt.UTC().Format(time.RFC3339))), + attribute.OptionalString("Service Account ID", reader.ServiceAccountID), + }) +} + func readerStatusBatteryLevel(v *float32) string { if v == nil { return "-" diff --git a/internal/commands/subaccounts/subaccounts.go b/internal/commands/subaccounts/subaccounts.go new file mode 100644 index 0000000..1c8113c --- /dev/null +++ b/internal/commands/subaccounts/subaccounts.go @@ -0,0 +1,396 @@ +package subaccounts + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/urfave/cli/v3" + + sumup "github.com/sumup/sumup-go" + + "github.com/sumup/sumup-cli/internal/app" + "github.com/sumup/sumup-cli/internal/commands/util" + "github.com/sumup/sumup-cli/internal/display" + "github.com/sumup/sumup-cli/internal/display/attribute" + "github.com/sumup/sumup-cli/internal/display/message" +) + +func NewCommand() *cli.Command { + return &cli.Command{ + Name: "subaccounts", + Usage: "Commands for the deprecated subaccounts API.", + Commands: []*cli.Command{ + { + Name: "list", + Usage: "List subaccounts for the authenticated merchant.", + Action: listSubaccounts, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "include-primary", + Usage: "Include the primary user in results.", + }, + &cli.StringFlag{ + Name: "query", + Usage: "Filter by email substring.", + }, + }, + }, + { + Name: "get", + Usage: "Get a subaccount by numeric ID.", + Action: getSubaccount, + ArgsUsage: "", + }, + { + Name: "create", + Usage: "Create a subaccount.", + Action: createSubaccount, + Flags: append(accountFlags(), permissionFlags("permission-")...), + }, + { + Name: "update", + Usage: "Update a subaccount.", + Action: updateSubaccount, + ArgsUsage: "", + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "username", + Usage: "Updated login email.", + }, + &cli.StringFlag{ + Name: "password", + Usage: "Updated password.", + }, + &cli.StringFlag{ + Name: "nickname", + Usage: "Updated nickname.", + }, + &cli.BoolFlag{ + Name: "disabled", + Usage: "Disable the subaccount.", + }, + &cli.BoolFlag{ + Name: "enabled", + Usage: "Enable the subaccount.", + }, + }, permissionFlags("permission-")...), + }, + }, + } +} + +func accountFlags() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: "username", + Usage: "Login email for the subaccount.", + Required: true, + }, + &cli.StringFlag{ + Name: "password", + Usage: "Password for the subaccount.", + Required: true, + }, + &cli.StringFlag{ + Name: "nickname", + Usage: "Optional nickname.", + }, + } +} + +func permissionFlags(prefix string) []cli.Flag { + return []cli.Flag{ + &cli.BoolFlag{Name: prefix + "create-moto-payments", Usage: "Grant create MOTO payments permission."}, + &cli.BoolFlag{Name: prefix + "create-referral", Usage: "Grant create referral permission."}, + &cli.BoolFlag{Name: prefix + "full-transaction-history-view", Usage: "Grant full transaction history permission."}, + &cli.BoolFlag{Name: prefix + "refund-transactions", Usage: "Grant refund transactions permission."}, + } +} + +func listSubaccounts(ctx context.Context, cmd *cli.Command) error { + appCtx, err := app.GetAppContext(cmd) + if err != nil { + return err + } + + params := sumup.SubaccountsListSubAccountsParams{} + if cmd.Bool("include-primary") { + value := true + params.IncludePrimary = &value + } + if query := cmd.String("query"); query != "" { + params.Query = &query + } + + response, err := appCtx.Client.Subaccounts.ListSubAccounts(ctx, params) + if err != nil { + return fmt.Errorf("list subaccounts: %w", err) + } + + if appCtx.JSONOutput { + return display.PrintJSON(response) + } + + rows := make([][]attribute.Value, 0, len(*response)) + for _, account := range *response { + rows = append(rows, []attribute.Value{ + attribute.ValueOf(account.ID), + attribute.ValueOf(account.Username), + attribute.ValueOf(nullableString(account.Nickname)), + attribute.ValueOf(account.AccountType), + attribute.ValueOf(boolLabel(account.Disabled)), + }) + } + + display.RenderTable( + "Subaccounts", + []string{"ID", "Username", "Nickname", "Type", "Disabled"}, + rows, + ) + return nil +} + +func getSubaccount(ctx context.Context, cmd *cli.Command) error { + appCtx, err := app.GetAppContext(cmd) + if err != nil { + return err + } + + operatorID, err := parseOperatorID(cmd) + if err != nil { + return err + } + + account, err := appCtx.Client.Subaccounts.CompatGetOperator(ctx, operatorID) + if err != nil { + return fmt.Errorf("get subaccount: %w", err) + } + + if appCtx.JSONOutput { + return display.PrintJSON(account) + } + + renderSubaccount(account) + return nil +} + +func createSubaccount(ctx context.Context, cmd *cli.Command) error { + appCtx, err := app.GetAppContext(cmd) + if err != nil { + return err + } + + body := sumup.SubaccountsCreateSubAccountParams{ + Username: cmd.String("username"), + Password: cmd.String("password"), + } + if nickname := cmd.String("nickname"); nickname != "" { + body.Nickname = &nickname + } + if permissions := createPermissions(cmd); permissions != nil { + body.Permissions = permissions + } + + account, err := appCtx.Client.Subaccounts.CreateSubAccount(ctx, body) + if err != nil { + return fmt.Errorf("create subaccount: %w", err) + } + + if appCtx.JSONOutput { + return display.PrintJSON(account) + } + + message.Success("Subaccount created") + renderSubaccount(account) + return nil +} + +func updateSubaccount(ctx context.Context, cmd *cli.Command) error { + appCtx, err := app.GetAppContext(cmd) + if err != nil { + return err + } + + operatorID, err := parseOperatorID(cmd) + if err != nil { + return err + } + + body := sumup.SubaccountsUpdateSubAccountParams{} + changeCount := 0 + if username := cmd.String("username"); username != "" { + body.Username = &username + changeCount++ + } + if password := cmd.String("password"); password != "" { + body.Password = &password + changeCount++ + } + if nickname := cmd.String("nickname"); nickname != "" { + body.Nickname = &nickname + changeCount++ + } + if cmd.Bool("disabled") && cmd.Bool("enabled") { + return fmt.Errorf("use either --disabled or --enabled, not both") + } + if cmd.Bool("disabled") { + value := true + body.Disabled = &value + changeCount++ + } + if cmd.Bool("enabled") { + value := false + body.Disabled = &value + changeCount++ + } + if permissions := updatePermissions(cmd); permissions != nil { + body.Permissions = permissions + changeCount++ + } + if changeCount == 0 { + return fmt.Errorf("no update fields provided") + } + + account, err := appCtx.Client.Subaccounts.UpdateSubAccount(ctx, operatorID, body) + if err != nil { + return fmt.Errorf("update subaccount: %w", err) + } + + if appCtx.JSONOutput { + return display.PrintJSON(account) + } + + message.Success("Subaccount updated") + renderSubaccount(account) + return nil +} + +func parseOperatorID(cmd *cli.Command) (int32, error) { + value, err := util.RequireSingleArg(cmd, "operator ID") + if err != nil { + return 0, err + } + var id int32 + if _, err := fmt.Sscanf(value, "%d", &id); err != nil { + return 0, fmt.Errorf("invalid operator ID %q", value) + } + return id, nil +} + +func createPermissions(cmd *cli.Command) *sumup.SubaccountsCreateSubAccountParamsPermissions { + var permissions sumup.SubaccountsCreateSubAccountParamsPermissions + changed := false + if cmd.Bool("permission-create-moto-payments") { + value := true + permissions.CreateMotoPayments = &value + changed = true + } + if cmd.Bool("permission-create-referral") { + value := true + permissions.CreateReferral = &value + changed = true + } + if cmd.Bool("permission-full-transaction-history-view") { + value := true + permissions.FullTransactionHistoryView = &value + changed = true + } + if cmd.Bool("permission-refund-transactions") { + value := true + permissions.RefundTransactions = &value + changed = true + } + if !changed { + return nil + } + return &permissions +} + +func updatePermissions(cmd *cli.Command) *sumup.SubaccountsUpdateSubAccountParamsPermissions { + var permissions sumup.SubaccountsUpdateSubAccountParamsPermissions + changed := false + if cmd.Bool("permission-create-moto-payments") { + value := true + permissions.CreateMotoPayments = &value + changed = true + } + if cmd.Bool("permission-create-referral") { + value := true + permissions.CreateReferral = &value + changed = true + } + if cmd.Bool("permission-full-transaction-history-view") { + value := true + permissions.FullTransactionHistoryView = &value + changed = true + } + if cmd.Bool("permission-refund-transactions") { + value := true + permissions.RefundTransactions = &value + changed = true + } + if !changed { + return nil + } + return &permissions +} + +func renderSubaccount(account *sumup.Operator) { + if account == nil { + return + } + + display.DataList([]attribute.KeyValue{ + attribute.ID(account.ID), + attribute.Attribute("Username", attribute.Styled(account.Username)), + attribute.Attribute("Nickname", attribute.Styled(nullableString(account.Nickname))), + attribute.Attribute("Type", attribute.Styled(account.AccountType)), + attribute.Attribute("Disabled", attribute.Styled(boolLabel(account.Disabled))), + attribute.Attribute("Permissions", attribute.Styled(renderPermissions(account.Permissions))), + attribute.Attribute("Created At", attribute.Styled(account.CreatedAt.UTC().Format(time.RFC3339))), + attribute.Attribute("Updated At", attribute.Styled(account.UpdatedAt.UTC().Format(time.RFC3339))), + }) +} + +func renderPermissions(permissions sumup.Permissions) string { + parts := make([]string, 0, 5) + if permissions.Admin { + parts = append(parts, "admin") + } + if permissions.CreateMotoPayments { + parts = append(parts, "create_moto_payments") + } + if permissions.CreateReferral { + parts = append(parts, "create_referral") + } + if permissions.FullTransactionHistoryView { + parts = append(parts, "full_transaction_history_view") + } + if permissions.RefundTransactions { + parts = append(parts, "refund_transactions") + } + if len(parts) == 0 { + return "-" + } + return strings.Join(parts, ", ") +} + +func nullableString(value interface{ Value() *string }) string { + if value == nil { + return "-" + } + current := value.Value() + if current == nil || *current == "" { + return "-" + } + return *current +} + +func boolLabel(value bool) string { + if value { + return "Yes" + } + return "No" +} diff --git a/internal/commands/transactions/transactions.go b/internal/commands/transactions/transactions.go index dfd0b93..9a11a6c 100644 --- a/internal/commands/transactions/transactions.go +++ b/internal/commands/transactions/transactions.go @@ -84,15 +84,43 @@ func NewCommand() *cli.Command { }, { Name: "get", - Usage: "Get a specific transaction by ID.", + Usage: "Get a specific transaction.", Action: getTransaction, - ArgsUsage: "", + ArgsUsage: "[transaction-id]", Flags: []cli.Flag{ &cli.StringFlag{ Name: "merchant-code", Usage: "Merchant code that owns the transaction. Falls back to context.", Sources: cli.EnvVars("SUMUP_MERCHANT_CODE"), }, + &cli.StringFlag{ + Name: "internal-id", + Usage: "Lookup by internal transaction ID.", + }, + &cli.StringFlag{ + Name: "transaction-code", + Usage: "Lookup by transaction code.", + }, + &cli.StringFlag{ + Name: "foreign-transaction-id", + Usage: "Lookup by foreign transaction ID.", + }, + &cli.StringFlag{ + Name: "client-transaction-id", + Usage: "Lookup by client transaction ID.", + }, + }, + }, + { + Name: "refund", + Usage: "Refund a transaction fully or partially.", + Action: refundTransaction, + ArgsUsage: "", + Flags: []cli.Flag{ + &cli.Float64Flag{ + Name: "amount", + Usage: "Optional partial refund amount in major units.", + }, }, }, }, @@ -213,12 +241,34 @@ func getTransaction(ctx context.Context, cmd *cli.Command) error { return err } - transactionID, err := util.RequireSingleArg(cmd, "transaction ID") - if err != nil { - return err + params := sumup.TransactionsGetParams{} + lookupCount := 0 + if cmd.Args().Len() > 0 { + transactionID := cmd.Args().Get(0) + params.ID = &transactionID + lookupCount++ + } + if value := cmd.String("internal-id"); value != "" { + params.InternalID = &value + lookupCount++ + } + if value := cmd.String("transaction-code"); value != "" { + params.TransactionCode = &value + lookupCount++ + } + if value := cmd.String("foreign-transaction-id"); value != "" { + params.ForeignTransactionID = &value + lookupCount++ } - params := sumup.TransactionsGetParams{ - ID: &transactionID, + if value := cmd.String("client-transaction-id"); value != "" { + params.ClientTransactionID = &value + lookupCount++ + } + if lookupCount == 0 { + return fmt.Errorf("provide a transaction ID argument or one lookup flag") + } + if lookupCount > 1 { + return fmt.Errorf("provide exactly one transaction lookup") } transaction, err := appCtx.Client.Transactions.Get(ctx, merchantCode, params) @@ -234,6 +284,33 @@ func getTransaction(ctx context.Context, cmd *cli.Command) error { return nil } +func refundTransaction(ctx context.Context, cmd *cli.Command) error { + appCtx, err := app.GetAppContext(cmd) + if err != nil { + return err + } + transactionID, err := util.RequireSingleArg(cmd, "transaction ID") + if err != nil { + return err + } + + body := sumup.TransactionsRefundParams{} + if cmd.IsSet("amount") { + value := float32(cmd.Float64("amount")) + body.Amount = &value + } + + if err := appCtx.Client.Transactions.Refund(ctx, transactionID, body); err != nil { + return fmt.Errorf("refund transaction: %w", err) + } + + if appCtx.JSONOutput { + return display.PrintJSON(map[string]string{"status": "refunded"}) + } + + return nil +} + func renderTransactionDetails(appCtx *app.Context, transaction *sumup.TransactionFull) { status := "-" if transaction.Status != nil && *transaction.Status != "" { From 660e963eb8349bb0a2e1da4601863866c69485b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Thu, 26 Mar 2026 14:25:30 +0100 Subject: [PATCH 2/2] refactor(cli): remove checkout card flow and subaccounts --- internal/commands/checkouts/checkouts.go | 30 -- internal/commands/commands.go | 2 - internal/commands/subaccounts/subaccounts.go | 396 ------------------- 3 files changed, 428 deletions(-) delete mode 100644 internal/commands/subaccounts/subaccounts.go diff --git a/internal/commands/checkouts/checkouts.go b/internal/commands/checkouts/checkouts.go index 7388c07..8bc87b1 100644 --- a/internal/commands/checkouts/checkouts.go +++ b/internal/commands/checkouts/checkouts.go @@ -137,12 +137,6 @@ func NewCommand() *cli.Command { &cli.StringFlag{Name: "phone", Usage: "Customer phone."}, &cli.StringFlag{Name: "tax-id", Usage: "Customer tax ID."}, &cli.StringFlag{Name: "birth-date", Usage: "Customer birth date in YYYY-MM-DD format."}, - &cli.StringFlag{Name: "card-name", Usage: "Cardholder name."}, - &cli.StringFlag{Name: "card-number", Usage: "Card number without spaces."}, - &cli.StringFlag{Name: "card-cvv", Usage: "Card CVV."}, - &cli.StringFlag{Name: "card-expiry-month", Usage: "Card expiry month in MM format."}, - &cli.StringFlag{Name: "card-expiry-year", Usage: "Card expiry year in YY or YYYY format."}, - &cli.StringFlag{Name: "card-zip-code", Usage: "Card ZIP code when required."}, }, }, }, @@ -387,11 +381,6 @@ func processCheckout(ctx context.Context, cmd *cli.Command) error { } else if changedCount > 0 { body.PersonalDetails = details } - if card, err := checkoutCardFromFlags(cmd); err != nil { - return err - } else if card != nil { - body.Card = card - } response, err := appCtx.Client.Checkouts.Process(ctx, checkoutID, body) if err != nil { @@ -484,25 +473,6 @@ func checkoutPersonalDetailsFromFlags(cmd *cli.Command) (*sumup.PersonalDetails, return details, changedCount, nil } -func checkoutCardFromFlags(cmd *cli.Command) (*sumup.Card, error) { - number := cmd.String("card-number") - if number == "" { - return nil, nil - } - - card := &sumup.Card{ - Number: number, - Name: cmd.String("card-name"), - Cvv: cmd.String("card-cvv"), - ExpiryMonth: sumup.CardExpiryMonth(cmd.String("card-expiry-month")), - ExpiryYear: cmd.String("card-expiry-year"), - } - if zipCode := cmd.String("card-zip-code"); zipCode != "" { - card.ZipCode = &zipCode - } - return card, nil -} - func enumString[T ~string](value *T) *string { if value == nil { return nil diff --git a/internal/commands/commands.go b/internal/commands/commands.go index a4802f9..766cc9c 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -13,7 +13,6 @@ import ( "github.com/sumup/sumup-cli/internal/commands/readers" "github.com/sumup/sumup-cli/internal/commands/receipts" "github.com/sumup/sumup-cli/internal/commands/roles" - "github.com/sumup/sumup-cli/internal/commands/subaccounts" "github.com/sumup/sumup-cli/internal/commands/transactions" "github.com/sumup/sumup-cli/internal/commands/version" ) @@ -31,7 +30,6 @@ func All() []*cli.Command { readers.NewCommand(), receipts.NewCommand(), roles.NewCommand(), - subaccounts.NewCommand(), transactions.NewCommand(), version.NewCommand(), } diff --git a/internal/commands/subaccounts/subaccounts.go b/internal/commands/subaccounts/subaccounts.go deleted file mode 100644 index 1c8113c..0000000 --- a/internal/commands/subaccounts/subaccounts.go +++ /dev/null @@ -1,396 +0,0 @@ -package subaccounts - -import ( - "context" - "fmt" - "strings" - "time" - - "github.com/urfave/cli/v3" - - sumup "github.com/sumup/sumup-go" - - "github.com/sumup/sumup-cli/internal/app" - "github.com/sumup/sumup-cli/internal/commands/util" - "github.com/sumup/sumup-cli/internal/display" - "github.com/sumup/sumup-cli/internal/display/attribute" - "github.com/sumup/sumup-cli/internal/display/message" -) - -func NewCommand() *cli.Command { - return &cli.Command{ - Name: "subaccounts", - Usage: "Commands for the deprecated subaccounts API.", - Commands: []*cli.Command{ - { - Name: "list", - Usage: "List subaccounts for the authenticated merchant.", - Action: listSubaccounts, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "include-primary", - Usage: "Include the primary user in results.", - }, - &cli.StringFlag{ - Name: "query", - Usage: "Filter by email substring.", - }, - }, - }, - { - Name: "get", - Usage: "Get a subaccount by numeric ID.", - Action: getSubaccount, - ArgsUsage: "", - }, - { - Name: "create", - Usage: "Create a subaccount.", - Action: createSubaccount, - Flags: append(accountFlags(), permissionFlags("permission-")...), - }, - { - Name: "update", - Usage: "Update a subaccount.", - Action: updateSubaccount, - ArgsUsage: "", - Flags: append([]cli.Flag{ - &cli.StringFlag{ - Name: "username", - Usage: "Updated login email.", - }, - &cli.StringFlag{ - Name: "password", - Usage: "Updated password.", - }, - &cli.StringFlag{ - Name: "nickname", - Usage: "Updated nickname.", - }, - &cli.BoolFlag{ - Name: "disabled", - Usage: "Disable the subaccount.", - }, - &cli.BoolFlag{ - Name: "enabled", - Usage: "Enable the subaccount.", - }, - }, permissionFlags("permission-")...), - }, - }, - } -} - -func accountFlags() []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{ - Name: "username", - Usage: "Login email for the subaccount.", - Required: true, - }, - &cli.StringFlag{ - Name: "password", - Usage: "Password for the subaccount.", - Required: true, - }, - &cli.StringFlag{ - Name: "nickname", - Usage: "Optional nickname.", - }, - } -} - -func permissionFlags(prefix string) []cli.Flag { - return []cli.Flag{ - &cli.BoolFlag{Name: prefix + "create-moto-payments", Usage: "Grant create MOTO payments permission."}, - &cli.BoolFlag{Name: prefix + "create-referral", Usage: "Grant create referral permission."}, - &cli.BoolFlag{Name: prefix + "full-transaction-history-view", Usage: "Grant full transaction history permission."}, - &cli.BoolFlag{Name: prefix + "refund-transactions", Usage: "Grant refund transactions permission."}, - } -} - -func listSubaccounts(ctx context.Context, cmd *cli.Command) error { - appCtx, err := app.GetAppContext(cmd) - if err != nil { - return err - } - - params := sumup.SubaccountsListSubAccountsParams{} - if cmd.Bool("include-primary") { - value := true - params.IncludePrimary = &value - } - if query := cmd.String("query"); query != "" { - params.Query = &query - } - - response, err := appCtx.Client.Subaccounts.ListSubAccounts(ctx, params) - if err != nil { - return fmt.Errorf("list subaccounts: %w", err) - } - - if appCtx.JSONOutput { - return display.PrintJSON(response) - } - - rows := make([][]attribute.Value, 0, len(*response)) - for _, account := range *response { - rows = append(rows, []attribute.Value{ - attribute.ValueOf(account.ID), - attribute.ValueOf(account.Username), - attribute.ValueOf(nullableString(account.Nickname)), - attribute.ValueOf(account.AccountType), - attribute.ValueOf(boolLabel(account.Disabled)), - }) - } - - display.RenderTable( - "Subaccounts", - []string{"ID", "Username", "Nickname", "Type", "Disabled"}, - rows, - ) - return nil -} - -func getSubaccount(ctx context.Context, cmd *cli.Command) error { - appCtx, err := app.GetAppContext(cmd) - if err != nil { - return err - } - - operatorID, err := parseOperatorID(cmd) - if err != nil { - return err - } - - account, err := appCtx.Client.Subaccounts.CompatGetOperator(ctx, operatorID) - if err != nil { - return fmt.Errorf("get subaccount: %w", err) - } - - if appCtx.JSONOutput { - return display.PrintJSON(account) - } - - renderSubaccount(account) - return nil -} - -func createSubaccount(ctx context.Context, cmd *cli.Command) error { - appCtx, err := app.GetAppContext(cmd) - if err != nil { - return err - } - - body := sumup.SubaccountsCreateSubAccountParams{ - Username: cmd.String("username"), - Password: cmd.String("password"), - } - if nickname := cmd.String("nickname"); nickname != "" { - body.Nickname = &nickname - } - if permissions := createPermissions(cmd); permissions != nil { - body.Permissions = permissions - } - - account, err := appCtx.Client.Subaccounts.CreateSubAccount(ctx, body) - if err != nil { - return fmt.Errorf("create subaccount: %w", err) - } - - if appCtx.JSONOutput { - return display.PrintJSON(account) - } - - message.Success("Subaccount created") - renderSubaccount(account) - return nil -} - -func updateSubaccount(ctx context.Context, cmd *cli.Command) error { - appCtx, err := app.GetAppContext(cmd) - if err != nil { - return err - } - - operatorID, err := parseOperatorID(cmd) - if err != nil { - return err - } - - body := sumup.SubaccountsUpdateSubAccountParams{} - changeCount := 0 - if username := cmd.String("username"); username != "" { - body.Username = &username - changeCount++ - } - if password := cmd.String("password"); password != "" { - body.Password = &password - changeCount++ - } - if nickname := cmd.String("nickname"); nickname != "" { - body.Nickname = &nickname - changeCount++ - } - if cmd.Bool("disabled") && cmd.Bool("enabled") { - return fmt.Errorf("use either --disabled or --enabled, not both") - } - if cmd.Bool("disabled") { - value := true - body.Disabled = &value - changeCount++ - } - if cmd.Bool("enabled") { - value := false - body.Disabled = &value - changeCount++ - } - if permissions := updatePermissions(cmd); permissions != nil { - body.Permissions = permissions - changeCount++ - } - if changeCount == 0 { - return fmt.Errorf("no update fields provided") - } - - account, err := appCtx.Client.Subaccounts.UpdateSubAccount(ctx, operatorID, body) - if err != nil { - return fmt.Errorf("update subaccount: %w", err) - } - - if appCtx.JSONOutput { - return display.PrintJSON(account) - } - - message.Success("Subaccount updated") - renderSubaccount(account) - return nil -} - -func parseOperatorID(cmd *cli.Command) (int32, error) { - value, err := util.RequireSingleArg(cmd, "operator ID") - if err != nil { - return 0, err - } - var id int32 - if _, err := fmt.Sscanf(value, "%d", &id); err != nil { - return 0, fmt.Errorf("invalid operator ID %q", value) - } - return id, nil -} - -func createPermissions(cmd *cli.Command) *sumup.SubaccountsCreateSubAccountParamsPermissions { - var permissions sumup.SubaccountsCreateSubAccountParamsPermissions - changed := false - if cmd.Bool("permission-create-moto-payments") { - value := true - permissions.CreateMotoPayments = &value - changed = true - } - if cmd.Bool("permission-create-referral") { - value := true - permissions.CreateReferral = &value - changed = true - } - if cmd.Bool("permission-full-transaction-history-view") { - value := true - permissions.FullTransactionHistoryView = &value - changed = true - } - if cmd.Bool("permission-refund-transactions") { - value := true - permissions.RefundTransactions = &value - changed = true - } - if !changed { - return nil - } - return &permissions -} - -func updatePermissions(cmd *cli.Command) *sumup.SubaccountsUpdateSubAccountParamsPermissions { - var permissions sumup.SubaccountsUpdateSubAccountParamsPermissions - changed := false - if cmd.Bool("permission-create-moto-payments") { - value := true - permissions.CreateMotoPayments = &value - changed = true - } - if cmd.Bool("permission-create-referral") { - value := true - permissions.CreateReferral = &value - changed = true - } - if cmd.Bool("permission-full-transaction-history-view") { - value := true - permissions.FullTransactionHistoryView = &value - changed = true - } - if cmd.Bool("permission-refund-transactions") { - value := true - permissions.RefundTransactions = &value - changed = true - } - if !changed { - return nil - } - return &permissions -} - -func renderSubaccount(account *sumup.Operator) { - if account == nil { - return - } - - display.DataList([]attribute.KeyValue{ - attribute.ID(account.ID), - attribute.Attribute("Username", attribute.Styled(account.Username)), - attribute.Attribute("Nickname", attribute.Styled(nullableString(account.Nickname))), - attribute.Attribute("Type", attribute.Styled(account.AccountType)), - attribute.Attribute("Disabled", attribute.Styled(boolLabel(account.Disabled))), - attribute.Attribute("Permissions", attribute.Styled(renderPermissions(account.Permissions))), - attribute.Attribute("Created At", attribute.Styled(account.CreatedAt.UTC().Format(time.RFC3339))), - attribute.Attribute("Updated At", attribute.Styled(account.UpdatedAt.UTC().Format(time.RFC3339))), - }) -} - -func renderPermissions(permissions sumup.Permissions) string { - parts := make([]string, 0, 5) - if permissions.Admin { - parts = append(parts, "admin") - } - if permissions.CreateMotoPayments { - parts = append(parts, "create_moto_payments") - } - if permissions.CreateReferral { - parts = append(parts, "create_referral") - } - if permissions.FullTransactionHistoryView { - parts = append(parts, "full_transaction_history_view") - } - if permissions.RefundTransactions { - parts = append(parts, "refund_transactions") - } - if len(parts) == 0 { - return "-" - } - return strings.Join(parts, ", ") -} - -func nullableString(value interface{ Value() *string }) string { - if value == nil { - return "-" - } - current := value.Value() - if current == nil || *current == "" { - return "-" - } - return *current -} - -func boolLabel(value bool) string { - if value { - return "Yes" - } - return "No" -}