From d5ebd344b0cd2a10f51348f2e98d0cecc051e25a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Sat, 7 Feb 2026 21:54:36 +0100 Subject: [PATCH] feat: update SumUp Go SDK --- README.md | 8 ++ go.mod | 9 +- go.sum | 10 +- internal/commands/checkouts/checkouts.go | 32 +++--- internal/commands/context/context.go | 4 +- internal/commands/customers/customers.go | 21 ++-- internal/commands/members/members.go | 22 +++-- internal/commands/memberships/memberships.go | 23 +++-- internal/commands/payouts/payouts.go | 54 +++-------- internal/commands/readers/readers.go | 97 ++++++++++++++++--- internal/commands/receipts/receipts.go | 26 ++--- internal/commands/roles/roles.go | 15 ++- .../commands/transactions/transactions.go | 54 +++++------ internal/display/attribute/attribute.go | 59 +++++++++-- internal/display/datalist.go | 3 +- internal/display/table.go | 26 ++++- 16 files changed, 301 insertions(+), 162 deletions(-) diff --git a/README.md b/README.md index b2f5bce..9dc2b70 100644 --- a/README.md +++ b/README.md @@ -88,4 +88,12 @@ sumup readers checkout \ When using affiliate attribution, pass all affiliate flags: `--affiliate-app-id`, `--affiliate-key`, and `--affiliate-foreign-transaction-id`. +Check the last known status of a paired reader: + +```bash +sumup readers status \ + --merchant-code M123 \ + reader_42 +``` + [docs-badge]: https://img.shields.io/badge/SumUp-documentation-white.svg?logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgY29sb3I9IndoaXRlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogICAgPHBhdGggZD0iTTIyLjI5IDBIMS43Qy43NyAwIDAgLjc3IDAgMS43MVYyMi4zYzAgLjkzLjc3IDEuNyAxLjcxIDEuN0gyMi4zYy45NCAwIDEuNzEtLjc3IDEuNzEtMS43MVYxLjdDMjQgLjc3IDIzLjIzIDAgMjIuMjkgMFptLTcuMjIgMTguMDdhNS42MiA1LjYyIDAgMCAxLTcuNjguMjQuMzYuMzYgMCAwIDEtLjAxLS40OWw3LjQ0LTcuNDRhLjM1LjM1IDAgMCAxIC40OSAwIDUuNiA1LjYgMCAwIDEtLjI0IDcuNjlabTEuNTUtMTEuOS03LjQ0IDcuNDVhLjM1LjM1IDAgMCAxLS41IDAgNS42MSA1LjYxIDAgMCAxIDcuOS03Ljk2bC4wMy4wM2MuMTMuMTMuMTQuMzUuMDEuNDlaIiBmaWxsPSJjdXJyZW50Q29sb3IiLz4KPC9zdmc+ diff --git a/go.mod b/go.mod index dfa6494..3cc5e8d 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/sumup/sumup-cli -go 1.24.0 - -toolchain go1.24.11 +go 1.25.5 require ( github.com/charmbracelet/bubbles v0.21.0 @@ -10,7 +8,7 @@ require ( github.com/charmbracelet/lipgloss v1.1.0 github.com/mergestat/timediff v0.0.4 github.com/shopspring/decimal v1.4.0 - github.com/sumup/sumup-go v0.9.0 + github.com/sumup/sumup-go v0.11.1 github.com/urfave/cli/v3 v3.6.2 golang.org/x/term v0.39.0 ) @@ -23,7 +21,6 @@ require ( github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/google/go-cmp v0.6.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect @@ -33,7 +30,7 @@ require ( github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.3.8 // indirect ) diff --git a/go.sum b/go.sum index 4391b55..8b5a39b 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -51,16 +49,16 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/sumup/sumup-go v0.9.0 h1:zD8AXGcJSzkiopI9ZNZMkWT4Xse1mpYe92G4dk27gJ4= -github.com/sumup/sumup-go v0.9.0/go.mod h1:UgT9aaxvUBrAiRpunqyb1zi5v/+1z+6Lr6p6bgL8EYI= +github.com/sumup/sumup-go v0.11.1 h1:jGthnT8V/QIllOoF4pE9kwS000AntdtEa4UTaejeX/I= +github.com/sumup/sumup-go v0.11.1/go.mod h1:UgT9aaxvUBrAiRpunqyb1zi5v/+1z+6Lr6p6bgL8EYI= github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8= github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= diff --git a/internal/commands/checkouts/checkouts.go b/internal/commands/checkouts/checkouts.go index 7936e14..263e541 100644 --- a/internal/commands/checkouts/checkouts.go +++ b/internal/commands/checkouts/checkouts.go @@ -38,8 +38,8 @@ func NewCommand() *cli.Command { Name: "create", Usage: "Create a new checkout resource.", Description: `Examples: - sumup-cli checkouts create --reference order-123 --amount 10 --currency EUR --merchant-code M123 - sumup-cli checkouts create --reference ticket-42 --amount 29.99 --currency EUR --merchant-code M123 --description "Ticket" --return-url https://example.com/return`, + sumup checkouts create --reference order-123 --amount 10 --currency EUR --merchant-code M123 + sumup checkouts create --reference ticket-42 --amount 29.99 --currency EUR --merchant-code M123 --description "Ticket" --return-url https://example.com/return`, Action: createCheckout, Flags: []cli.Flag{ &cli.StringFlag{ @@ -113,23 +113,27 @@ func listCheckouts(ctx context.Context, cmd *cli.Command) error { return display.PrintJSON(checkoutList) } - rows := make([][]string, 0, len(*checkoutList)) + rows := make([][]attribute.Value, 0, len(*checkoutList)) for _, checkout := range *checkoutList { status := "-" if checkout.Status != nil { status = string(*checkout.Status) } - rows = append(rows, []string{ - util.StringOrDefault(checkout.ID, "-"), - util.StringOrDefault(checkout.CheckoutReference, "-"), - currency.FormatPointers(checkout.Amount, checkout.Currency), - status, - util.StringOrDefault(checkout.MerchantCode, "-"), - util.TimeOrDash(appCtx, checkout.Date), + rows = append(rows, []attribute.Value{ + attribute.OptionalStringValue(checkout.ID), + attribute.OptionalStringValue(checkout.CheckoutReference), + attribute.ValueOf(currency.FormatPointers(checkout.Amount, checkout.Currency)), + attribute.ValueOf(status), + attribute.OptionalStringValue(checkout.MerchantCode), + attribute.ValueOf(util.TimeOrDash(appCtx, checkout.Date)), }) } - display.RenderTable("Checkouts", []string{"ID", "Reference", "Amount", "Status", "Merchant", "Created At"}, rows) + display.RenderTable( + "Checkouts", + []string{"ID", "Reference", "Amount", "Status", "Merchant", "Created At"}, + rows, + ) return nil } @@ -160,13 +164,13 @@ func createCheckout(ctx context.Context, cmd *cli.Command) error { body.Description = &value } if value := cmd.String("return-url"); value != "" { - body.ReturnUrl = &value + body.ReturnURL = &value } if value := cmd.String("redirect-url"); value != "" { - body.RedirectUrl = &value + body.RedirectURL = &value } if value := cmd.String("customer-id"); value != "" { - body.CustomerId = &value + body.CustomerID = &value } if value := cmd.String("purpose"); value != "" { purpose := checkouts.CreateCheckoutBodyPurpose(value) diff --git a/internal/commands/context/context.go b/internal/commands/context/context.go index a88cf2c..f5f6a70 100644 --- a/internal/commands/context/context.go +++ b/internal/commands/context/context.go @@ -157,7 +157,7 @@ func (m model) searchMemberships(query string, parentID string, parentType membe } if parentID != "" { - params.ResourceParentId = &parentID + params.ResourceParentID = &parentID } if parentType != "" { @@ -350,7 +350,7 @@ func (m model) View() string { } s.WriteString("\n") } else if len(items) > maxVisible { - s.WriteString(fmt.Sprintf("\n(Showing %d-%d of %d)", start+1, end, len(items))) + fmt.Fprintf(&s, "\n(Showing %d-%d of %d)", start+1, end, len(items)) } helpStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("241")) diff --git a/internal/commands/customers/customers.go b/internal/commands/customers/customers.go index 01fbe8a..45b9652 100644 --- a/internal/commands/customers/customers.go +++ b/internal/commands/customers/customers.go @@ -11,6 +11,7 @@ import ( "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" ) func NewCommand() *cli.Command { @@ -46,18 +47,22 @@ func listPaymentInstruments(ctx context.Context, cmd *cli.Command) error { return display.PrintJSON(instruments) } - rows := make([][]string, 0, len(*instruments)) + rows := make([][]attribute.Value, 0, len(*instruments)) for _, instrument := range *instruments { - rows = append(rows, []string{ - util.StringOrDefault(instrument.Token, "-"), - paymentInstrumentType(&instrument), - lastFour(&instrument), - util.BoolLabel(instrument.Active), - util.TimeOrDash(appCtx, instrument.CreatedAt), + rows = append(rows, []attribute.Value{ + attribute.OptionalStringValue(instrument.Token), + attribute.ValueOf(paymentInstrumentType(&instrument)), + attribute.ValueOf(lastFour(&instrument)), + attribute.ValueOf(util.BoolLabel(instrument.Active)), + attribute.ValueOf(util.TimeOrDash(appCtx, instrument.CreatedAt)), }) } - display.RenderTable("Payment Instruments", []string{"Token", "Type", "Last 4", "Active", "Created At"}, rows) + display.RenderTable( + "Payment Instruments", + []string{"Token", "Type", "Last 4", "Active", "Created At"}, + rows, + ) return nil } diff --git a/internal/commands/members/members.go b/internal/commands/members/members.go index c079bb9..1bd1196 100644 --- a/internal/commands/members/members.go +++ b/internal/commands/members/members.go @@ -153,7 +153,7 @@ func listMembers(ctx context.Context, cmd *cli.Command) error { params.Email = &value } if value := cmd.String("user-id"); value != "" { - params.UserId = &value + params.UserID = &value } if roles := cmd.StringSlice("role"); len(roles) > 0 { params.Roles = roles @@ -179,18 +179,22 @@ func listMembers(ctx context.Context, cmd *cli.Command) error { return display.PrintJSON(response.Items) } - rows := make([][]string, 0, len(response.Items)) + rows := make([][]attribute.Value, 0, len(response.Items)) for _, member := range response.Items { - rows = append(rows, []string{ - member.ID, - memberEmail(member), - memberRoles(member.Roles), - membershipStatusLabel(member.Status), - member.CreatedAt.UTC().Format(time.RFC3339), + rows = append(rows, []attribute.Value{ + attribute.ValueOf(member.ID), + attribute.ValueOf(memberEmail(member)), + attribute.ValueOf(memberRoles(member.Roles)), + attribute.ValueOf(membershipStatusLabel(member.Status)), + attribute.ValueOf(member.CreatedAt.UTC().Format(time.RFC3339)), }) } - display.RenderTable("Members", []string{"ID", "Email", "Roles", "Status", "Created At"}, rows) + display.RenderTable( + "Members", + []string{"ID", "Email", "Roles", "Status", "Created At"}, + rows, + ) return nil } diff --git a/internal/commands/memberships/memberships.go b/internal/commands/memberships/memberships.go index 0576d20..abeb81a 100644 --- a/internal/commands/memberships/memberships.go +++ b/internal/commands/memberships/memberships.go @@ -13,6 +13,7 @@ import ( "github.com/sumup/sumup-cli/internal/app" "github.com/sumup/sumup-cli/internal/display" + "github.com/sumup/sumup-cli/internal/display/attribute" ) func NewCommand() *cli.Command { @@ -105,19 +106,23 @@ func listMemberships(ctx context.Context, cmd *cli.Command) error { return display.PrintJSON(response) } - rows := make([][]string, 0, len(response.Items)) + rows := make([][]attribute.Value, 0, len(response.Items)) for _, membership := range response.Items { - rows = append(rows, []string{ - membership.ID, - membership.Resource.Name, - string(membership.Resource.Type), - memberRoles(membership.Roles), - membershipStatusLabel(membership.Status), - membership.CreatedAt.UTC().Format(time.RFC3339), + rows = append(rows, []attribute.Value{ + attribute.ValueOf(membership.ID), + attribute.ValueOf(membership.Resource.Name), + attribute.ValueOf(string(membership.Resource.Type)), + attribute.ValueOf(memberRoles(membership.Roles)), + attribute.ValueOf(membershipStatusLabel(membership.Status)), + attribute.ValueOf(membership.CreatedAt.UTC().Format(time.RFC3339)), }) } - display.RenderTable("Memberships", []string{"ID", "Resource", "Type", "Roles", "Status", "Created At"}, rows) + display.RenderTable( + "Memberships", + []string{"ID", "Resource", "Type", "Roles", "Status", "Created At"}, + rows, + ) return nil } diff --git a/internal/commands/payouts/payouts.go b/internal/commands/payouts/payouts.go index 6f8c917..bcfc894 100644 --- a/internal/commands/payouts/payouts.go +++ b/internal/commands/payouts/payouts.go @@ -12,8 +12,8 @@ import ( "github.com/sumup/sumup-go/payouts" "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" ) func NewCommand() *cli.Command { @@ -94,20 +94,24 @@ func listPayouts(ctx context.Context, cmd *cli.Command) error { return display.PrintJSON(payoutList) } - rows := make([][]string, 0, len(*payoutList)) + rows := make([][]attribute.Value, 0, len(*payoutList)) for _, payout := range *payoutList { - rows = append(rows, []string{ - intPointerToString(payout.ID), - dateOrDash(payout.Date), - payoutAmount(payout), - floatPointerToString(payout.Fee), - enumOrDash(payout.Status), - enumOrDash(payout.Type), - util.StringOrDefault(payout.Reference, "-"), + rows = append(rows, []attribute.Value{ + attribute.OptionalValue(payout.ID, func(v int) string { return fmt.Sprintf("%d", v) }), + attribute.OptionalValue(payout.Date, func(d datetime.Date) string { return d.String() }), + attribute.ValueOf(payoutAmount(payout)), + attribute.OptionalValue(payout.Fee, func(v float32) string { return fmt.Sprintf("%.2f", v) }), + attribute.OptionalValue(payout.Status, func(v payouts.FinancialPayoutStatus) string { return string(v) }), + attribute.OptionalValue(payout.Type, func(v payouts.FinancialPayoutType) string { return string(v) }), + attribute.OptionalStringValue(payout.Reference), }) } - display.RenderTable("Payouts", []string{"ID", "Date", "Amount", "Fee", "Status", "Type", "Reference"}, rows) + display.RenderTable( + "Payouts", + []string{"ID", "Date", "Amount", "Fee", "Status", "Type", "Reference"}, + rows, + ) return nil } @@ -119,34 +123,6 @@ func parseDateArg(value string) (datetime.Date, error) { return datetime.Date{Time: parsed}, nil } -func intPointerToString(value *int) string { - if value == nil { - return "-" - } - return fmt.Sprintf("%d", *value) -} - -func floatPointerToString(value *float32) string { - if value == nil { - return "-" - } - return fmt.Sprintf("%.2f", *value) -} - -func enumOrDash[T ~string](value *T) string { - if value == nil { - return "-" - } - return string(*value) -} - -func dateOrDash(value *datetime.Date) string { - if value == nil { - return "-" - } - return value.String() -} - func payoutAmount(payout payouts.FinancialPayout) string { if payout.Amount == nil { return "-" diff --git a/internal/commands/readers/readers.go b/internal/commands/readers/readers.go index 0f87ca9..9c0bd0c 100644 --- a/internal/commands/readers/readers.go +++ b/internal/commands/readers/readers.go @@ -2,6 +2,7 @@ package readers import ( "context" + "errors" "fmt" "math" "strings" @@ -9,6 +10,7 @@ import ( "github.com/urfave/cli/v3" "github.com/sumup/sumup-go/readers" + "github.com/sumup/sumup-go/shared" "github.com/sumup/sumup-cli/internal/app" "github.com/sumup/sumup-cli/internal/commands/util" @@ -73,6 +75,20 @@ func NewCommand() *cli.Command { }, }, }, + { + Name: "status", + Usage: "Show the last known status of a reader.", + Action: readerStatus, + 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.", @@ -156,20 +172,24 @@ func listReaders(ctx context.Context, cmd *cli.Command) error { return display.PrintJSON(response.Items) } - rows := make([][]string, 0, len(response.Items)) + rows := make([][]attribute.Value, 0, len(response.Items)) for _, reader := range response.Items { name := string(reader.Name) model := string(reader.Device.Model) - rows = append(rows, []string{ - string(reader.ID), - name, - string(reader.Status), - model, - reader.Device.Identifier, + rows = append(rows, []attribute.Value{ + attribute.ValueOf(string(reader.ID)), + attribute.ValueOf(name), + attribute.ValueOf(string(reader.Status)), + attribute.ValueOf(model), + attribute.ValueOf(reader.Device.Identifier), }) } - display.RenderTable("Readers", []string{"ID", "Name", "Status", "Model", "Identifier"}, rows) + display.RenderTable( + "Readers", + []string{"ID", "Name", "Status", "Model", "Identifier"}, + rows, + ) return nil } @@ -184,7 +204,9 @@ func addReader(ctx context.Context, cmd *cli.Command) error { } reader, err := appCtx.Client.Readers.Create(ctx, cmd.String("merchant-code"), body) - if err != nil { + if pErr := new(shared.Problem); errors.As(err, &pErr) { + return fmt.Errorf("create reader: %v %v", *pErr.Detail, *pErr.Title) + } else if err != nil { return fmt.Errorf("create reader: %w", err) } @@ -213,7 +235,7 @@ func deleteReader(ctx context.Context, cmd *cli.Command) error { return err } - err = appCtx.Client.Readers.Delete(ctx, cmd.String("merchant-code"), readers.ReaderId(readerID)) + err = appCtx.Client.Readers.Delete(ctx, cmd.String("merchant-code"), readers.ReaderID(readerID)) if err != nil { return fmt.Errorf("delete reader: %w", err) } @@ -259,7 +281,7 @@ func readerCheckout(ctx context.Context, cmd *cli.Command) error { body.Description = &desc } if returnURL := cmd.String("return-url"); returnURL != "" { - body.ReturnUrl = &returnURL + body.ReturnURL = &returnURL } if cardType := cmd.String("card-type"); cardType != "" { ct := readers.CreateReaderCheckoutBodyCardType(cardType) @@ -309,6 +331,55 @@ func readerCheckout(ctx context.Context, cmd *cli.Command) error { return nil } +func readerStatus(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 + } + + response, err := appCtx.Client.Readers.GetStatus(ctx, cmd.String("merchant-code"), readerID, readers.GetReaderStatusParams{}) + if err != nil { + return fmt.Errorf("get reader status: %w", err) + } + + if appCtx.JSONOutput { + return display.PrintJSON(response) + } + + data := response.Data + details := []attribute.KeyValue{ + attribute.ID(readerID), + attribute.Attribute("Status", attribute.Styled(string(data.Status))), + attribute.Optional("State", data.State, func(v readers.StatusResponseDataState) string { + return string(v) + }), + attribute.Optional("Connection", data.ConnectionType, func(v readers.StatusResponseDataConnectionType) string { + return string(v) + }), + attribute.Attribute("Battery Level", attribute.Styled(readerStatusBatteryLevel(data.BatteryLevel))), + attribute.Optional("Battery Temp", data.BatteryTemperature, func(v int) string { + return fmt.Sprintf("%d°C", v) + }), + attribute.OptionalString("Firmware", data.FirmwareVersion), + attribute.Attribute("Last Activity", attribute.Styled(util.TimeOrDash(appCtx, data.LastActivity))), + } + + display.DataList(details) + return nil +} + +func readerStatusBatteryLevel(v *float32) string { + if v == nil { + return "-" + } + + return fmt.Sprintf("%.0f%%", *v) +} + func buildAffiliatePayload(cmd *cli.Command) (*readers.CreateReaderCheckoutBodyAffiliate, error) { appID := cmd.String("affiliate-app-id") key := cmd.String("affiliate-key") @@ -320,8 +391,8 @@ func buildAffiliatePayload(cmd *cli.Command) (*readers.CreateReaderCheckoutBodyA return nil, fmt.Errorf("affiliate requires --affiliate-app-id, --affiliate-key, and --affiliate-foreign-transaction-id") } return &readers.CreateReaderCheckoutBodyAffiliate{ - AppId: appID, + AppID: appID, Key: key, - ForeignTransactionId: foreignID, + ForeignTransactionID: foreignID, }, nil } diff --git a/internal/commands/receipts/receipts.go b/internal/commands/receipts/receipts.go index e9dcc93..017f771 100644 --- a/internal/commands/receipts/receipts.go +++ b/internal/commands/receipts/receipts.go @@ -62,7 +62,7 @@ func getReceipt(ctx context.Context, cmd *cli.Command) error { } if cmd.IsSet("transaction-event-id") { value := cmd.Int("transaction-event-id") - params.TxEventId = &value + params.TxEventID = &value } receipt, err := appCtx.Client.Receipts.Get(ctx, transactionID, params) @@ -82,13 +82,13 @@ func renderReceipt(receipt *receipts.Receipt) { if transaction := receipt.TransactionData; transaction != nil { fmt.Println("Transaction") display.DataList([]attribute.KeyValue{ - attribute.Attribute("Code", attribute.Styled(util.StringOrDefault(transaction.TransactionCode, "-"))), - attribute.Attribute("Status", attribute.Styled(util.StringOrDefault(transaction.Status, "-"))), - attribute.Attribute("Payment Type", attribute.Styled(util.StringOrDefault(transaction.PaymentType, "-"))), + attribute.OptionalString("Code", transaction.TransactionCode), + attribute.OptionalString("Status", transaction.Status), + attribute.OptionalString("Payment Type", transaction.PaymentType), attribute.Attribute("Amount", attribute.Styled(receiptAmount(transaction))), attribute.Attribute("Timestamp", attribute.Styled(timePointerToString(transaction.Timestamp))), - attribute.Attribute("Entry Mode", attribute.Styled(util.StringOrDefault(transaction.EntryMode, "-"))), - attribute.Attribute("Verification", attribute.Styled(util.StringOrDefault(transaction.VerificationMethod, "-"))), + attribute.OptionalString("Entry Mode", transaction.EntryMode), + attribute.OptionalString("Verification", transaction.VerificationMethod), attribute.Attribute("Card", attribute.Styled(receiptCard(transaction))), }) } else { @@ -99,14 +99,14 @@ func renderReceipt(receipt *receipts.Receipt) { fmt.Println("\nMerchant") pairs := make([]attribute.KeyValue, 0, 5) if profile := merchant.MerchantProfile; profile != nil { - pairs = append(pairs, attribute.Attribute("Name", attribute.Styled(util.StringOrDefault(profile.BusinessName, "-")))) - pairs = append(pairs, attribute.Attribute("Code", attribute.Styled(util.StringOrDefault(profile.MerchantCode, "-")))) + pairs = append(pairs, attribute.OptionalString("Name", profile.BusinessName)) + pairs = append(pairs, attribute.OptionalString("Code", profile.MerchantCode)) if address := profile.Address; address != nil { if formatted := formatAddress(address); formatted != "" { pairs = append(pairs, attribute.Attribute("Address", attribute.Styled(formatted))) } } - pairs = append(pairs, attribute.Attribute("Email", attribute.Styled(util.StringOrDefault(profile.Email, "-")))) + pairs = append(pairs, attribute.OptionalString("Email", profile.Email)) } else { fmt.Println("Merchant profile unavailable") } @@ -119,10 +119,10 @@ func renderReceipt(receipt *receipts.Receipt) { if acquirer := receipt.AcquirerData; acquirer != nil { fmt.Println("\nAcquirer") display.DataList([]attribute.KeyValue{ - attribute.Attribute("Terminal ID", attribute.Styled(util.StringOrDefault(acquirer.Tid, "-"))), - attribute.Attribute("Authorization Code", attribute.Styled(util.StringOrDefault(acquirer.AuthorizationCode, "-"))), - attribute.Attribute("Return Code", attribute.Styled(util.StringOrDefault(acquirer.ReturnCode, "-"))), - attribute.Attribute("Local Time", attribute.Styled(util.StringOrDefault(acquirer.LocalTime, "-"))), + attribute.OptionalString("Terminal ID", acquirer.Tid), + attribute.OptionalString("Authorization Code", acquirer.AuthorizationCode), + attribute.OptionalString("Return Code", acquirer.ReturnCode), + attribute.OptionalString("Local Time", acquirer.LocalTime), }) } diff --git a/internal/commands/roles/roles.go b/internal/commands/roles/roles.go index 0570785..f67ef3b 100644 --- a/internal/commands/roles/roles.go +++ b/internal/commands/roles/roles.go @@ -7,6 +7,7 @@ import ( "github.com/sumup/sumup-cli/internal/app" "github.com/sumup/sumup-cli/internal/display" + "github.com/sumup/sumup-cli/internal/display/attribute" ) type role struct { @@ -40,12 +41,20 @@ func listRoles(_ context.Context, cmd *cli.Command) error { return display.PrintJSON(roles) } - rows := make([][]string, 0, len(roles)) + rows := make([][]attribute.Value, 0, len(roles)) for _, role := range roles { - rows = append(rows, []string{role.Name, role.DisplayName, role.Description}) + rows = append(rows, []attribute.Value{ + attribute.ValueOf(role.Name), + attribute.ValueOf(role.DisplayName), + attribute.ValueOf(role.Description), + }) } - display.RenderTable("Roles", []string{"Role", "Display Name", "Description"}, rows) + display.RenderTable( + "Roles", + []string{"Role", "Display Name", "Description"}, + rows, + ) return nil } diff --git a/internal/commands/transactions/transactions.go b/internal/commands/transactions/transactions.go index c7a4705..ab32067 100644 --- a/internal/commands/transactions/transactions.go +++ b/internal/commands/transactions/transactions.go @@ -8,6 +8,7 @@ import ( "github.com/urfave/cli/v3" + "github.com/sumup/sumup-go/shared" "github.com/sumup/sumup-go/transactions" "github.com/sumup/sumup-cli/internal/app" @@ -143,7 +144,16 @@ func listTransactions(ctx context.Context, cmd *cli.Command) error { params.Order = &value } if values := cmd.StringSlice("payment-type"); len(values) > 0 { - params.PaymentTypes = values + types := make([]shared.PaymentType, 0, len(values)) + for _, v := range values { + if v == "" { + continue + } + types = append(types, shared.PaymentType(v)) + } + if len(types) > 0 { + params.PaymentTypes = types + } } if values := cmd.StringSlice("status"); len(values) > 0 { params.Statuses = values @@ -173,19 +183,23 @@ func listTransactions(ctx context.Context, cmd *cli.Command) error { return display.PrintJSON(items) } - rows := make([][]string, 0, len(items)) + rows := make([][]attribute.Value, 0, len(items)) for _, tx := range items { - rows = append(rows, []string{ - util.StringOrDefault(tx.ID, "-"), - util.StringOrDefault(tx.TransactionCode, "-"), - currency.FormatPointers(tx.Amount, tx.Currency), - transactionHistoryStatus(tx.Status), - transactionHistoryPaymentType(tx.PaymentType), - util.TimeOrDash(appCtx, tx.Timestamp), + rows = append(rows, []attribute.Value{ + attribute.OptionalStringValue(tx.ID), + attribute.OptionalStringValue(tx.TransactionCode), + attribute.ValueOf(currency.FormatPointers(tx.Amount, tx.Currency)), + attribute.OptionalValue(tx.Status, func(v transactions.TransactionHistoryStatus) string { return string(v) }), + attribute.OptionalValue(tx.PaymentType, func(v shared.PaymentType) string { return string(v) }), + attribute.ValueOf(util.TimeOrDash(appCtx, tx.Timestamp)), }) } - display.RenderTable("Transactions", []string{"ID", "Code", "Amount", "Status", "Payment Type", "Created At"}, rows) + display.RenderTable( + "Transactions", + []string{"ID", "Code", "Amount", "Status", "Payment Type", "Created At"}, + rows, + ) return nil } @@ -221,20 +235,6 @@ func getTransaction(ctx context.Context, cmd *cli.Command) error { return nil } -func transactionHistoryStatus(status *transactions.TransactionHistoryStatus) string { - if status == nil { - return "-" - } - return string(*status) -} - -func transactionHistoryPaymentType(paymentType *transactions.TransactionHistoryPaymentType) string { - if paymentType == nil { - return "-" - } - return string(*paymentType) -} - func renderTransactionDetails(appCtx *app.Context, transaction *transactions.TransactionFull) { status := "-" if transaction.Status != nil && *transaction.Status != "" { @@ -248,12 +248,12 @@ func renderTransactionDetails(appCtx *app.Context, transaction *transactions.Tra display.DataList([]attribute.KeyValue{ attribute.ID(util.StringOrDefault(transaction.ID, "-")), attribute.Attribute("Status", attribute.Styled(status)), - attribute.Attribute("Code", attribute.Styled(util.StringOrDefault(transaction.TransactionCode, "-"))), + attribute.OptionalString("Code", transaction.TransactionCode), attribute.Attribute("Amount", attribute.Styled(currency.FormatPointers(transaction.Amount, transaction.Currency))), - attribute.Attribute("Merchant", attribute.Styled(util.StringOrDefault(transaction.MerchantCode, "-"))), + attribute.OptionalString("Merchant", transaction.MerchantCode), attribute.Attribute("Payment Type", attribute.Styled(paymentType)), attribute.Attribute("Card", attribute.Styled(transactionCardLabel(transaction.Card))), - attribute.Attribute("Description", attribute.Styled(util.StringOrDefault(transaction.ProductSummary, "-"))), + attribute.OptionalString("Description", transaction.ProductSummary), attribute.Attribute("Created At", attribute.Styled(util.TimeOrDash(appCtx, transaction.Timestamp))), }) } diff --git a/internal/display/attribute/attribute.go b/internal/display/attribute/attribute.go index 736a26e..10b158c 100644 --- a/internal/display/attribute/attribute.go +++ b/internal/display/attribute/attribute.go @@ -11,6 +11,15 @@ var ( idStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF61F2")).Bold(true) ) +type Value struct { + Text string + Style lipgloss.Style +} + +func (v Value) String() string { + return v.Style.Render(v.Text) +} + type styled[T any] struct { V T Style lipgloss.Style @@ -20,9 +29,9 @@ func (s styled[T]) String() string { return s.Style.Render(fmt.Sprintf("%v", s.V)) } -func (s styled[T]) toStyledString() styled[string] { - return styled[string]{ - V: fmt.Sprintf("%v", s.V), +func (s styled[T]) toValue() Value { + return Value{ + Text: fmt.Sprintf("%v", s.V), Style: s.Style, } } @@ -39,18 +48,18 @@ func Styled[T any](v T, style ...lipgloss.Style) styled[T] { } func ID[T any](v T) KeyValue { - return Attribute("ID", Styled(v, idStyle).toStyledString()) + return Attribute("ID", Styled(v, idStyle)) } type KeyValue struct { - Key styled[string] - Value styled[string] + Key Value + Value Value } func Attribute[T any](key string, value styled[T]) KeyValue { return KeyValue{ - Key: Styled(key, keyStyle), - Value: value.toStyledString(), + Key: Styled(key, keyStyle).toValue(), + Value: value.toValue(), } } @@ -65,3 +74,37 @@ func Bool(key string, value styled[bool]) KeyValue { func Stringer(key string, value styled[fmt.Stringer]) KeyValue { return Attribute(key, value) } + +func ValueOf[T any](v T, style ...lipgloss.Style) Value { + return Styled(v, style...).toValue() +} + +func OptionalValue[T any](value *T, formatter func(T) string) Value { + if value == nil { + return ValueOf("-") + } + return ValueOf(formatter(*value)) +} + +func OptionalStringValue(value *string) Value { + if value == nil || *value == "" { + return ValueOf("-") + } + return ValueOf(*value) +} + +// Optional renders the provided pointer using formatter. Missing values are displayed as "-". +func Optional[T any](key string, value *T, formatter func(T) string) KeyValue { + return KeyValue{ + Key: Styled(key, keyStyle).toValue(), + Value: OptionalValue(value, formatter), + } +} + +// OptionalString renders string pointers, treating nil or empty values as "-". +func OptionalString(key string, value *string) KeyValue { + return KeyValue{ + Key: Styled(key, keyStyle).toValue(), + Value: OptionalStringValue(value), + } +} diff --git a/internal/display/datalist.go b/internal/display/datalist.go index 2b25520..fe961db 100644 --- a/internal/display/datalist.go +++ b/internal/display/datalist.go @@ -2,6 +2,7 @@ package display import ( "fmt" + "strings" "github.com/sumup/sumup-cli/internal/display/attribute" ) @@ -13,7 +14,7 @@ func DataList(pairs []attribute.KeyValue) { } for _, pair := range pairs { - if pair.Key.V == "" { + if strings.TrimSpace(pair.Key.Text) == "" { continue } fmt.Printf("%s: %s\n", pair.Key.String(), pair.Value.String()) diff --git a/internal/display/table.go b/internal/display/table.go index c1be1c5..5a8c3f2 100644 --- a/internal/display/table.go +++ b/internal/display/table.go @@ -6,12 +6,14 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/table" + + "github.com/sumup/sumup-cli/internal/display/attribute" ) const fallbackWidth = 120 // RenderTable prints rows in a table using the terminal width to wrap columns. -func RenderTable(title string, headers []string, rows [][]string) { +func RenderTable(title string, headers []string, rows [][]attribute.Value) { if len(rows) == 0 { fmt.Printf("%s: No items to display\n", title) return @@ -34,23 +36,39 @@ func RenderTable(title string, headers []string, rows [][]string) { } } + stringRows := make([][]string, len(rows)) + for i, row := range rows { + stringRows[i] = make([]string, len(headers)) + for j := range headers { + if j < len(row) { + stringRows[i][j] = row[j].Text + continue + } + stringRows[i][j] = "-" + } + } + t := table.New(). Border(lipgloss.HiddenBorder()). BorderRow(false). BorderColumn(false). BorderHeader(false). Headers(headers...). - Rows(rows...). + Rows(stringRows...). Width(width). Wrap(false). StyleFunc(func(row, col int) lipgloss.Style { if row == table.HeaderRow { return headerStyle } + style := defaultStyle + if rowIndex := row - 1; rowIndex >= 0 && rowIndex < len(rows) && col >= 0 && col < len(rows[rowIndex]) { + style = style.Inherit(rows[rowIndex][col].Style) + } if col >= 0 && col < len(idColumns) && idColumns[col] { - return idStyle + style = style.Inherit(idStyle) } - return defaultStyle + return style }) fmt.Println(title)