From 96c9ea721647019ec64b54e813b0874067ef675d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Thu, 20 Aug 2026 13:45:49 +0200 Subject: [PATCH] fix: add missing go card reader checkout --- README.md | 11 + go.mod | 4 +- go.sum | 8 +- internal/apicommands/catalog.gen.go | 87 ++++--- internal/codesamples/codesamples.go | 35 ++- internal/codesamples/codesamples_test.go | 2 +- internal/commands/checkouts/checkouts.go | 124 ---------- internal/commands/readers/readers.go | 234 +++++++++++++++++- .../commands/readers/readers_internal_test.go | 2 +- 9 files changed, 311 insertions(+), 196 deletions(-) diff --git a/README.md b/README.md index 186d1d4..fce77da 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,17 @@ sumup readers checkout \ When using affiliate attribution, pass all affiliate flags: `--affiliate-app-id`, `--affiliate-key`, and `--affiliate-foreign-transaction-id`. +Trigger a checkout on a SumUp Go reader. `--client-transaction-id` is required and used as the idempotency key: + +```bash +sumup readers go-checkout \ + --merchant-code M123 \ + --amount 14.99 \ + --currency EUR \ + --client-transaction-id 19e12390-72cf-4f9f-80b5-b0c8a67fa43f \ + reader_42 +``` + Check the last known status of a paired reader: ```bash diff --git a/go.mod b/go.mod index d13684c..865b62d 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/mergestat/timediff v0.0.4 github.com/shopspring/decimal v1.4.0 github.com/stretchr/testify v1.11.1 - github.com/sumup/sumup-go v0.17.2 + github.com/sumup/sumup-go v0.19.0 github.com/urfave/cli-docs/v3 v3.1.0 github.com/urfave/cli/v3 v3.10.1 golang.org/x/term v0.45.0 @@ -34,7 +34,7 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.47.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index aa55be4..caa206a 100644 --- a/go.sum +++ b/go.sum @@ -48,8 +48,8 @@ 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.17.2 h1:OpxHJ5yX0N2aCgrWhGqHcKoElMFHT38QanvVOd0e3pM= -github.com/sumup/sumup-go v0.17.2/go.mod h1:D++G2u9zvobimuUrgvSbah/tS3ON2Dmi6yb6ScA2Jpg= +github.com/sumup/sumup-go v0.19.0 h1:/AT4t6R4geRDdAtXiEI/JgOK/PyfPd7va77KEjzYoqU= +github.com/sumup/sumup-go v0.19.0/go.mod h1:D++G2u9zvobimuUrgvSbah/tS3ON2Dmi6yb6ScA2Jpg= github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw= github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to= github.com/urfave/cli/v3 v3.10.1 h1:7Kx9H50hrHbRbyxgO1KP6/BcbiGRz0uYh5YyQ30JEEY= @@ -58,8 +58,8 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= -golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= diff --git a/internal/apicommands/catalog.gen.go b/internal/apicommands/catalog.gen.go index addb24e..999503d 100644 --- a/internal/apicommands/catalog.gen.go +++ b/internal/apicommands/catalog.gen.go @@ -1,13 +1,13 @@ -// Code generated by generate-operations from github.com/sumup/sumup-go v0.17.2. DO NOT EDIT. -// OpenAPI SHA-256: 65a93b57821333a273b1c50ac435b16808dedab3e0e27bd7d42b053cf1507b36 +// Code generated by generate-operations from github.com/sumup/sumup-go v0.19.0. DO NOT EDIT. +// OpenAPI SHA-256: b050b86c35d18907a2a6b9f633bd1460568f37e450465047947c2aefad98d72e package apicommands const ( CatalogSDKModule = "github.com/sumup/sumup-go" - CatalogSDKVersion = "v0.17.2" + CatalogSDKVersion = "v0.19.0" CatalogOpenAPIVersion = "1.0.0" - CatalogSpecSHA256 = "65a93b57821333a273b1c50ac435b16808dedab3e0e27bd7d42b053cf1507b36" + CatalogSpecSHA256 = "b050b86c35d18907a2a6b9f633bd1460568f37e450465047947c2aefad98d72e" ) var Operations = []Operation{ @@ -30,7 +30,7 @@ var Operations = []Operation{ Summary: "Create an Apple Pay session", Description: "Creates an Apple Pay merchant session for the specified checkout.\n\nUse this endpoint after the customer selects Apple Pay and before calling\n`ApplePaySession.completeMerchantValidation(...)` in the browser.\nSumUp validates the merchant session request and returns the Apple Pay\nsession object that your frontend should pass to Apple's JavaScript API.", Parameters: []Parameter{ - {Name: "checkout_id", Location: "path", Description: "Unique ID of the checkout resource.", Type: "string", Format: "", Required: true}, + {Name: "checkout_id", Location: "path", Description: "Unique identifier of the checkout resource.", Type: "string", Format: "", Required: true}, }, RequestBody: &RequestBody{Schema: "object", Required: false}, }, @@ -43,7 +43,7 @@ var Operations = []Operation{ Summary: "Deactivate a checkout", Description: "Deactivates an identified checkout resource. If the checkout has already been processed it can not be deactivated.", Parameters: []Parameter{ - {Name: "checkout_id", Location: "path", Description: "Unique ID of the checkout resource.", Type: "string", Format: "", Required: true}, + {Name: "checkout_id", Location: "path", Description: "Unique identifier of the checkout resource.", Type: "string", Format: "", Required: true}, }, }, { @@ -55,7 +55,7 @@ var Operations = []Operation{ Summary: "Retrieve a checkout", Description: "Retrieves an identified checkout resource. Use this request after processing a checkout to confirm its status and inform the end user respectively.", Parameters: []Parameter{ - {Name: "checkout_id", Location: "path", Description: "Unique ID of the checkout resource.", Type: "string", Format: "", Required: true}, + {Name: "checkout_id", Location: "path", Description: "Unique identifier of the checkout resource.", Type: "string", Format: "", Required: true}, }, }, { @@ -67,7 +67,7 @@ var Operations = []Operation{ Summary: "List checkouts", Description: "Lists created checkout resources according to the applied `checkout_reference`.", Parameters: []Parameter{ - {Name: "checkout_reference", Location: "query", Description: "Filters the list of checkout resources by the unique ID of the checkout.", Type: "string", Format: "", Required: false}, + {Name: "checkout_reference", Location: "query", Description: "Filters the list of checkout resources by the unique reference of the checkout.", Type: "string", Format: "", Required: false}, }, }, { @@ -79,24 +79,11 @@ var Operations = []Operation{ Summary: "Get available payment methods", Description: "Get payment methods available for the given merchant to use with a checkout.", Parameters: []Parameter{ - {Name: "merchant_code", Location: "path", Description: "The SumUp merchant code.", Type: "string", Format: "", Required: true}, + {Name: "merchant_code", Location: "path", Description: "Short unique identifier for the merchant.", Type: "string", Format: "", Required: true}, {Name: "amount", Location: "query", Description: "The amount for which the payment methods should be eligible, in major units.", Type: "number", Format: "", Required: false}, {Name: "currency", Location: "query", Description: "The currency for which the payment methods should be eligible.", Type: "string", Format: "", Required: false}, }, }, - { - ID: "ProcessCheckout", - Client: "Checkouts", - SDKMethod: "Process", - HTTPMethod: "PUT", - Path: "/v0.1/checkouts/{checkout_id}", - Summary: "Process a checkout", - Description: "Processing a checkout will attempt to charge the provided payment instrument for the amount of the specified checkout resource initiated in the `Create a checkout` endpoint.\n\nFollow this request with `Retrieve a checkout` to confirm its status.", - Parameters: []Parameter{ - {Name: "checkout_id", Location: "path", Description: "Unique ID of the checkout resource.", Type: "string", Format: "", Required: true}, - }, - RequestBody: &RequestBody{Schema: "ProcessCheckout", Required: true}, - }, { ID: "UpdateCheckout", Client: "Checkouts", @@ -106,7 +93,7 @@ var Operations = []Operation{ Summary: "Update a checkout", Description: "Updates an identified checkout resource.", Parameters: []Parameter{ - {Name: "checkout_id", Location: "path", Description: "Unique ID of the checkout resource.", Type: "string", Format: "", Required: true}, + {Name: "checkout_id", Location: "path", Description: "Unique identifier of the checkout resource.", Type: "string", Format: "", Required: true}, }, RequestBody: &RequestBody{Schema: "CheckoutUpdateRequest", Required: true}, }, @@ -129,7 +116,7 @@ var Operations = []Operation{ Summary: "Deactivate a payment instrument", Description: "Deactivates an identified card payment instrument resource for a customer.", Parameters: []Parameter{ - {Name: "customer_id", Location: "path", Description: "Unique ID of the saved customer resource.", Type: "string", Format: "", Required: true}, + {Name: "customer_id", Location: "path", Description: "Unique identifier of the saved customer resource.", Type: "string", Format: "", Required: true}, {Name: "token", Location: "path", Description: "Unique token identifying the card saved as a payment instrument resource.", Type: "string", Format: "", Required: true}, }, }, @@ -142,7 +129,7 @@ var Operations = []Operation{ Summary: "Retrieve a customer", Description: "Retrieves an identified saved customer resource through the unique `customer_id` parameter, generated upon customer creation.", Parameters: []Parameter{ - {Name: "customer_id", Location: "path", Description: "Unique ID of the saved customer resource.", Type: "string", Format: "", Required: true}, + {Name: "customer_id", Location: "path", Description: "Unique identifier of the saved customer resource.", Type: "string", Format: "", Required: true}, }, }, { @@ -154,7 +141,7 @@ var Operations = []Operation{ Summary: "List payment instruments", Description: "Lists all payment instrument resources that are saved for an identified customer.", Parameters: []Parameter{ - {Name: "customer_id", Location: "path", Description: "Unique ID of the saved customer resource.", Type: "string", Format: "", Required: true}, + {Name: "customer_id", Location: "path", Description: "Unique identifier of the saved customer resource.", Type: "string", Format: "", Required: true}, }, }, { @@ -166,7 +153,7 @@ var Operations = []Operation{ Summary: "Update a customer", Description: "Updates an identified saved customer resource's personal details.\n\nThe request only overwrites the parameters included in the request, all other parameters will remain with their initially assigned values.", Parameters: []Parameter{ - {Name: "customer_id", Location: "path", Description: "Unique ID of the saved customer resource.", Type: "string", Format: "", Required: true}, + {Name: "customer_id", Location: "path", Description: "Unique identifier of the saved customer resource.", Type: "string", Format: "", Required: true}, }, RequestBody: &RequestBody{Schema: "object", Required: true}, }, @@ -312,7 +299,7 @@ var Operations = []Operation{ Summary: "List payouts", Description: "Lists payout and payout-deduction records for the specified merchant account within the requested date range.\n\nThe response can include:\n- regular payouts (`type = PAYOUT`)\n- deduction records for refunds, chargebacks, direct debit returns, or balance adjustments\n\nResults are sorted by payout date in the requested `order`.", Parameters: []Parameter{ - {Name: "merchant_code", Location: "path", Description: "Merchant code of the account whose payouts should be listed.", Type: "string", Format: "", Required: true}, + {Name: "merchant_code", Location: "path", Description: "Short unique identifier for the merchant.", Type: "string", Format: "", Required: true}, {Name: "start_date", Location: "query", Description: "Start date of the payout period filter, inclusive, in [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) `date` format (`YYYY-MM-DD`).", Type: "string", Format: "date", Required: true}, {Name: "end_date", Location: "query", Description: "End date of the payout period filter, inclusive, in [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) `date` format (`YYYY-MM-DD`). Must be greater than or equal to `start_date`.", Type: "string", Format: "date", Required: true}, {Name: "format", Location: "query", Description: "Response format for the payout list.", Type: "string", Format: "", Required: false}, @@ -347,6 +334,20 @@ var Operations = []Operation{ }, RequestBody: &RequestBody{Schema: "CreateReaderCheckoutRequest", Required: true}, }, + { + ID: "CreateGoReaderCheckout", + Client: "Readers", + SDKMethod: "CreateGoCheckout", + HTTPMethod: "POST", + Path: "/v0/merchants/{merchant_code}/readers/{reader_id}/go-checkout", + Summary: "Create a Go Reader Payment", + Description: "Initiates a payment on the SumUp Go terminal identified by the reader ID.\n\nUse `client_transaction_id` as an idempotency key: retrying the request with the same value returns the result of the original payment instead of creating a duplicate.", + Parameters: []Parameter{ + {Name: "merchant_code", Location: "path", Description: "Short unique identifier for the merchant.", Type: "string", Format: "", Required: true}, + {Name: "reader_id", Location: "path", Description: "The unique identifier of the reader.", Type: "ReaderID", Format: "", Required: true}, + }, + RequestBody: &RequestBody{Schema: "ReaderPaymentRequestParams", Required: true}, + }, { ID: "DeleteReader", Client: "Readers", @@ -374,6 +375,20 @@ var Operations = []Operation{ {Name: "reader_id", Location: "path", Description: "The unique identifier of the reader.", Type: "ReaderID", Format: "", Required: true}, }, }, + { + ID: "GetReaderCheckout", + Client: "Readers", + SDKMethod: "GetCheckout", + HTTPMethod: "GET", + Path: "/v0.1/merchants/{merchant_code}/readers/{reader_id}/checkout/{checkout_id}", + Summary: "Get a Reader Checkout", + Description: "Get a Checkout for a Reader.", + Parameters: []Parameter{ + {Name: "merchant_code", Location: "path", Description: "Merchant Code", Type: "string", Format: "", Required: true}, + {Name: "reader_id", Location: "path", Description: "The unique identifier of the Reader", Type: "string", Format: "", Required: true}, + {Name: "checkout_id", Location: "path", Description: "The unique identifier of the Checkout", Type: "string", Format: "", Required: true}, + }, + }, { ID: "GetReaderStatus", Client: "Readers", @@ -437,8 +452,8 @@ var Operations = []Operation{ Description: "Retrieves receipt specific data for a transaction.", Parameters: []Parameter{ {Name: "transaction_id", Location: "path", Description: "SumUp unique transaction ID or transaction code, e.g. TS7HDYLSKD.", Type: "string", Format: "", Required: true}, - {Name: "mid", Location: "query", Description: "Merchant code.", Type: "string", Format: "", Required: true}, - {Name: "tx_event_id", Location: "query", Description: "The ID of the transaction event (refund).", Type: "integer", Format: "", Required: false}, + {Name: "mid", Location: "query", Description: "Short unique identifier for the merchant.", Type: "string", Format: "", Required: true}, + {Name: "tx_event_id", Location: "query", Description: "Unique identifier of the transaction event to include on the receipt.", Type: "integer", Format: "", Required: false}, }, }, { @@ -515,11 +530,11 @@ var Operations = []Operation{ Summary: "Retrieve a transaction", Description: "Retrieves the full details of an identified transaction. The transaction resource is identified by a query parameter and *one* of following parameters is required:\n- `id`\n- `transaction_code`\n- `foreign_transaction_id`\n- `client_transaction_id`", Parameters: []Parameter{ - {Name: "merchant_code", Location: "path", Description: "Merchant code of the account whose transaction should be retrieved.", Type: "string", Format: "", Required: true}, + {Name: "merchant_code", Location: "path", Description: "Short unique identifier for the merchant.", Type: "string", Format: "", Required: true}, {Name: "id", Location: "query", Description: "Retrieves the transaction resource with the specified transaction ID (the `id` parameter in the transaction resource).", Type: "string", Format: "", Required: false}, {Name: "transaction_code", Location: "query", Description: "Retrieves the transaction resource with the specified transaction code.", Type: "string", Format: "", Required: false}, - {Name: "foreign_transaction_id", Location: "query", Description: "External/foreign transaction id (passed by clients).", Type: "string", Format: "", Required: false}, - {Name: "client_transaction_id", Location: "query", Description: "Client transaction id.", Type: "string", Format: "", Required: false}, + {Name: "foreign_transaction_id", Location: "query", Description: "External transaction identifier supplied by the client.", Type: "string", Format: "", Required: false}, + {Name: "client_transaction_id", Location: "query", Description: "Client-supplied identifier of the transaction.", Type: "string", Format: "", Required: false}, }, }, { @@ -531,7 +546,7 @@ var Operations = []Operation{ Summary: "List transactions", Description: "Lists detailed history of all transactions associated with the merchant profile.", Parameters: []Parameter{ - {Name: "merchant_code", Location: "path", Description: "Merchant code of the account whose transaction history should be listed.", Type: "string", Format: "", Required: true}, + {Name: "merchant_code", Location: "path", Description: "Short unique identifier for the merchant.", Type: "string", Format: "", Required: true}, {Name: "transaction_code", Location: "query", Description: "Retrieves the transaction resource with the specified transaction code.", Type: "string", Format: "", Required: false}, {Name: "order", Location: "query", Description: "Specifies the order in which the returned results are displayed.", Type: "string", Format: "", Required: false}, {Name: "limit", Location: "query", Description: "Specifies the maximum number of results per page. Value must be a positive integer and if not specified, will return 10 results.", Type: "integer", Format: "", Required: false}, @@ -556,8 +571,8 @@ var Operations = []Operation{ Summary: "Refund a transaction", Description: "Refunds an identified transaction either in full or partially.", Parameters: []Parameter{ - {Name: "merchant_code", Location: "path", Description: "Merchant code of the account that owns the payment to refund.", Type: "string", Format: "", Required: true}, - {Name: "transaction_id", Location: "path", Description: "Unique ID of the transaction.", Type: "string", Format: "", Required: true}, + {Name: "merchant_code", Location: "path", Description: "Short unique identifier for the merchant.", Type: "string", Format: "", Required: true}, + {Name: "transaction_id", Location: "path", Description: "Unique identifier of the transaction.", Type: "string", Format: "", Required: true}, }, RequestBody: &RequestBody{Schema: "object", Required: false}, }, diff --git a/internal/codesamples/codesamples.go b/internal/codesamples/codesamples.go index 261eb0b..cc11a83 100644 --- a/internal/codesamples/codesamples.go +++ b/internal/codesamples/codesamples.go @@ -75,10 +75,6 @@ var optionalSampleFlags = map[string]map[string]string{ "checkouts update": { "description": "Updated order", }, - "checkouts process": { - "customer-id": "$CUSTOMER_ID", - "token": "$PAYMENT_INSTRUMENT_TOKEN", - }, "customers create": { "email": "customer@example.com", }, @@ -94,21 +90,22 @@ var optionalSampleFlags = map[string]map[string]string{ } var flagSampleValues = map[string]string{ - "amount": "10.00", - "context": "example.com", - "currency": "EUR", - "email": "member@example.com", - "end-date": "2026-01-31", - "merchant-code": "$SUMUP_MERCHANT_CODE", - "name": "Example", - "pairing-code": "4WLFDSBF", - "password": "$MEMBER_PASSWORD", - "payment-type": "card", - "permission": "members_access", - "reference": "order-123", - "role": "role_employee", - "start-date": "2026-01-01", - "target": "https://apple-pay-gateway-cert.apple.com/paymentservices/startSession", + "amount": "10.00", + "client-transaction-id": "19e12390-72cf-4f9f-80b5-b0c8a67fa43f", + "context": "example.com", + "currency": "EUR", + "email": "member@example.com", + "end-date": "2026-01-31", + "merchant-code": "$SUMUP_MERCHANT_CODE", + "name": "Example", + "pairing-code": "4WLFDSBF", + "password": "$MEMBER_PASSWORD", + "payment-type": "card", + "permission": "members_access", + "reference": "order-123", + "role": "role_employee", + "start-date": "2026-01-01", + "target": "https://apple-pay-gateway-cert.apple.com/paymentservices/startSession", } var argumentSampleValues = map[string]string{ diff --git a/internal/codesamples/codesamples_test.go b/internal/codesamples/codesamples_test.go index 75026fc..7a20852 100644 --- a/internal/codesamples/codesamples_test.go +++ b/internal/codesamples/codesamples_test.go @@ -65,7 +65,7 @@ func TestGenerate(t *testing.T) { --merchant-code "$SUMUP_MERCHANT_CODE" `, sampleByID(t, catalog.Samples, "GetPerson").Source) assert.Contains(t, sampleByID(t, catalog.Samples, "UpdateCheckout").Source, `--description "Updated order"`) - assert.Contains(t, sampleByID(t, catalog.Samples, "ProcessCheckout").Source, `--token "$PAYMENT_INSTRUMENT_TOKEN"`) + assert.Contains(t, sampleByID(t, catalog.Samples, "CreateGoReaderCheckout").Source, "sumup readers go-checkout") assert.Contains(t, sampleByID(t, catalog.Samples, "CreateMerchantMember").Source, "sumup members create") assert.NotContains(t, sampleByID(t, catalog.Samples, "CreateMerchantMember").Source, "members invite") diff --git a/internal/commands/checkouts/checkouts.go b/internal/commands/checkouts/checkouts.go index ff6a4b9..bb4d55f 100644 --- a/internal/commands/checkouts/checkouts.go +++ b/internal/commands/checkouts/checkouts.go @@ -10,7 +10,6 @@ import ( "github.com/urfave/cli/v3" sumup "github.com/sumup/sumup-go" - "github.com/sumup/sumup-go/datetime" "github.com/sumup/sumup-go/nullable" "github.com/sumup/sumup-cli/internal/apicommands" @@ -19,7 +18,6 @@ import ( "github.com/sumup/sumup-cli/internal/currency" "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 { @@ -185,28 +183,6 @@ func NewCommand() *cli.Command { }, }, }), - apicommands.Bind("ProcessCheckout", &cli.Command{ - 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."}, - }, - }), }, } } @@ -521,66 +497,6 @@ func listPaymentMethods(ctx context.Context, cmd *cli.Command) error { }) } -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 - } - - 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(appCtx.Output, response) - } - - if response.CheckoutSuccess != nil { - if err := message.Success(appCtx.StatusOutput, "Checkout processed"); err != nil { - return err - } - return renderCheckout(appCtx, response.CheckoutSuccess) - } - - if response.CheckoutAccepted != nil { - if err := message.Success(appCtx.StatusOutput, "Checkout accepted"); err != nil { - return err - } - if response.CheckoutAccepted.NextStep != nil { - return display.DataList(appCtx.Output, []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) error { if checkout == nil { return nil @@ -627,46 +543,6 @@ func checkoutMutationDetails(appCtx *app.Context, checkout *sumup.Checkout) []at return details.Pairs() } -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 parseRFC3339Time(value string, label string) (time.Time, error) { parsed, err := time.Parse(time.RFC3339, value) if err != nil { diff --git a/internal/commands/readers/readers.go b/internal/commands/readers/readers.go index c386051..1c76d69 100644 --- a/internal/commands/readers/readers.go +++ b/internal/commands/readers/readers.go @@ -199,6 +199,64 @@ func NewCommand() *cli.Command { }, }, }), + apicommands.Bind("CreateGoReaderCheckout", &cli.Command{ + Name: "go-checkout", + Usage: "Trigger a checkout on a SumUp Go reader.", + Action: goReaderCheckout, + ArgsUsage: "", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "merchant-code", + Usage: "Merchant code that owns the reader. Falls back to context.", + Sources: cli.EnvVars("SUMUP_MERCHANT_CODE"), + }, + &cli.StringFlag{ + Name: "amount", + Usage: "Amount to charge, expressed in major units (for example 14.99).", + Required: true, + }, + &cli.IntFlag{ + Name: "minor-unit", + Usage: "Number of decimal places for the currency (for example 2 for EUR).", + Value: 2, + }, + &cli.StringFlag{ + Name: "currency", + Usage: fmt.Sprintf("Currency used for the transaction amount. Supported: %s", strings.Join(currency.Supported(), ", ")), + Required: true, + }, + &cli.StringFlag{ + Name: "client-transaction-id", + Usage: "Caller-supplied correlation identifier, used as the idempotency key.", + Required: true, + }, + &cli.StringFlag{ + Name: "tip-amount", + Usage: "Optional tip amount in major units, added on top of the total amount.", + }, + &cli.StringFlag{ + Name: "affiliate-app-id", + Usage: "Affiliate app ID to attribute the transaction.", + }, + &cli.StringFlag{ + Name: "affiliate-key", + Usage: "Affiliate key to attribute the transaction.", + }, + }, + }), + apicommands.Bind("GetReaderCheckout", &cli.Command{ + Name: "get-checkout", + Usage: "Get a checkout for a reader.", + Action: getReaderCheckout, + ArgsUsage: " ", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "merchant-code", + Usage: "Merchant code that owns the reader. Falls back to context.", + Sources: cli.EnvVars("SUMUP_MERCHANT_CODE"), + }, + }, + }), }, } } @@ -318,23 +376,16 @@ func readerCheckout(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } - parsedCurrency, err := currency.Parse(cmd.String("currency")) + parsedCurrency, value, err := parseAmountMinorUnits(cmd, cmd.String("amount")) if err != nil { return err } - value, err := currency.ToMinorUnits(cmd.String("amount"), int32(cmd.Int("minor-unit"))) - if err != nil { - return err - } - if value > int64(math.MaxInt32) || value < int64(math.MinInt32) { - return fmt.Errorf("amount is too large to convert into minor units") - } body := sumup.ReadersCreateCheckoutParams{ TotalAmount: sumup.CreateCheckoutRequestTotalAmount{ Currency: currency.Code(parsedCurrency), MinorUnit: cmd.Int("minor-unit"), - Value: int(value), + Value: value, }, } @@ -392,6 +443,99 @@ func readerCheckout(ctx context.Context, cmd *cli.Command) error { return display.DataList(appCtx.Output, details) } +func goReaderCheckout(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 + } + readerID, err := util.RequireSingleArg(cmd, "reader ID") + if err != nil { + return err + } + parsedCurrency, value, err := parseAmountMinorUnits(cmd, cmd.String("amount")) + if err != nil { + return err + } + + body := sumup.ReadersCreateGoCheckoutParams{ + ClientTransactionID: cmd.String("client-transaction-id"), + TotalAmount: sumup.Amount{ + Currency: currency.Code(parsedCurrency), + Value: value, + }, + } + if cmd.IsSet("tip-amount") { + _, tipValue, err := parseAmountMinorUnits(cmd, cmd.String("tip-amount")) + if err != nil { + return fmt.Errorf("tip amount: %w", err) + } + body.TipAmount = &tipValue + } + + affiliate, err := buildGoAffiliatePayload(cmd) + if err != nil { + return err + } + if affiliate != nil { + body.Affiliate = affiliate + } + + response, err := appCtx.Client.Readers.CreateGoCheckout(ctx, merchantCode, sumup.ReaderID(readerID), body) + if err != nil { + return fmt.Errorf("trigger go reader checkout: %w", err) + } + + details := []attribute.KeyValue{ + attribute.Attribute("Amount", attribute.Styled(currency.Format(float64(value)/math.Pow10(cmd.Int("minor-unit")), parsedCurrency))), + attribute.Attribute("Client Transaction ID", attribute.Styled(cmd.String("client-transaction-id"))), + } + if response != nil && response.Data != nil { + if response.Data.TransactionCode != nil && *response.Data.TransactionCode != "" { + details = append(details, attribute.Attribute("Transaction Code", attribute.Styled(*response.Data.TransactionCode))) + } + } + + return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{ + JSONValue: response, + SuccessMessage: "Go reader checkout initiated", + Details: details, + }) +} + +func getReaderCheckout(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 + } + if cmd.Args().Len() != 2 { + return fmt.Errorf("expected exactly 2 arguments: reader ID and checkout ID") + } + readerID := cmd.Args().Get(0) + checkoutID := cmd.Args().Get(1) + + response, err := appCtx.Client.Readers.GetCheckout(ctx, merchantCode, readerID, checkoutID) + if err != nil { + return fmt.Errorf("get reader checkout: %w", err) + } + + if appCtx.JSONOutput { + return display.PrintJSON(appCtx.Output, response) + } + if response == nil { + return nil + } + + return renderReaderCheckout(appCtx, appCtx.Output, &response.Data) +} + func readerStatus(ctx context.Context, cmd *cli.Command) error { appCtx, err := app.GetAppContext(cmd) if err != nil { @@ -562,3 +706,75 @@ func buildAffiliatePayload(cmd *cli.Command) (*sumup.CreateCheckoutRequestAffili ForeignTransactionID: foreignID, }, nil } + +func buildGoAffiliatePayload(cmd *cli.Command) (*sumup.Affiliate, error) { + appID := cmd.String("affiliate-app-id") + key := cmd.String("affiliate-key") + if appID == "" && key == "" { + return nil, nil + } + if appID == "" || key == "" { + return nil, fmt.Errorf("affiliate requires --affiliate-app-id and --affiliate-key") + } + return &sumup.Affiliate{ + AppID: appID, + Key: key, + }, nil +} + +func parseAmountMinorUnits(cmd *cli.Command, amount string) (sumup.Currency, int, error) { + parsedCurrency, err := currency.Parse(cmd.String("currency")) + if err != nil { + return "", 0, err + } + value, err := currency.ToMinorUnits(amount, int32(cmd.Int("minor-unit"))) + if err != nil { + return "", 0, err + } + if value > int64(math.MaxInt32) || value < int64(math.MinInt32) { + return "", 0, fmt.Errorf("amount is too large to convert into minor units") + } + return parsedCurrency, int(value), nil +} + +func renderReaderCheckout(appCtx *app.Context, w io.Writer, data *sumup.GetReaderCheckoutResponseData) error { + if data == nil { + return nil + } + + majorAmount := float64(data.TotalAmount.Value) / math.Pow10(data.TotalAmount.MinorUnit) + amountText := fmt.Sprintf("%.*f %s", data.TotalAmount.MinorUnit, majorAmount, data.TotalAmount.Currency) + if parsedCurrency, err := currency.Parse(data.TotalAmount.Currency); err == nil { + amountText = currency.Format(majorAmount, parsedCurrency) + } + + details := display.NewDetailsBuilder(). + AddID(data.CheckoutID). + Add("Status", attribute.Styled(string(data.Status))). + Add("Payment Type", attribute.Styled(string(data.PaymentType))). + Add("Amount", attribute.Styled(amountText)). + Add("Client Transaction ID", attribute.Styled(data.ClientTransactionID)). + Add("Created At", attribute.Styled(util.TimeOrDash(appCtx, &data.CreatedAt))). + Add("Updated At", attribute.Styled(util.TimeOrDash(appCtx, &data.UpdatedAt))). + Add("Reader Serial", attribute.Styled(data.ReaderSerialNumber)). + Add("Firmware", attribute.Styled(data.ReaderFirmwareVersion)) + if cardType := data.CardType.Value(); cardType != nil { + details.Add("Card Type", attribute.Styled(string(*cardType))) + } + if installments := data.Installments.Value(); installments != nil { + details.Add("Installments", attribute.Styled(*installments)) + } + if paymentStatus := data.PaymentStatus.Value(); paymentStatus != nil { + details.Add("Payment Status", attribute.Styled(*paymentStatus)) + } + if data.PaymentFailureReason != nil { + if reason := data.PaymentFailureReason.Value(); reason != nil && *reason != "" { + details.Add("Failure Reason", attribute.Styled(*reason)) + } + } + if validUntil := data.ValidUntil.Value(); validUntil != nil { + details.Add("Valid Until", attribute.Styled(util.TimeOrDash(appCtx, validUntil))) + } + + return details.Render(w) +} diff --git a/internal/commands/readers/readers_internal_test.go b/internal/commands/readers/readers_internal_test.go index 3ae4aee..63380f1 100644 --- a/internal/commands/readers/readers_internal_test.go +++ b/internal/commands/readers/readers_internal_test.go @@ -17,7 +17,7 @@ func TestNewCommand(t *testing.T) { t.Run("reader context-aware commands do not require merchant code", func(t *testing.T) { cmd := NewCommand() - for _, name := range []string{"get", "update", "terminate"} { + for _, name := range []string{"get", "update", "terminate", "go-checkout", "get-checkout"} { subcommand := findSubcommand(t, cmd, name) flag := findStringFlag(t, subcommand, "merchant-code") assert.False(t, flag.Required, "%s merchant-code flag should allow context fallback", name)