From 6c2328495be1ec411935620cff17b97e0005698c Mon Sep 17 00:00:00 2001 From: matst80 Date: Fri, 18 Apr 2025 20:41:25 +0200 Subject: [PATCH] refactor klarna stuff --- cart-grain.go | 11 ++- go.mod | 3 +- go.sum | 2 - klarna-client.go | 104 +++++++++++++++++++++++++++ klarna-push-type.go | 70 ------------------- klarna-types.go | 167 ++++++++++++++++++++++++++++++++++++++++++++ main.go | 36 +++------- pool-server.go | 54 ++++---------- 8 files changed, 302 insertions(+), 145 deletions(-) create mode 100644 klarna-client.go delete mode 100644 klarna-push-type.go create mode 100644 klarna-types.go diff --git a/cart-grain.go b/cart-grain.go index 725fec7..7d2ca8a 100644 --- a/cart-grain.go +++ b/cart-grain.go @@ -9,7 +9,6 @@ import ( "time" messages "git.tornberg.me/go-cart-actor/proto" - klarna "github.com/Flaconi/go-klarna" ) type CartId [16]byte @@ -430,7 +429,7 @@ func (c *CartGrain) HandleMessage(message *Message, isReplay bool) (*FrameWithPa err = fmt.Errorf("expected CreateCheckoutOrder") } else { - orderLines := make([]*klarna.Line, 0, len(c.Items)) + orderLines := make([]*Line, 0, len(c.Items)) totalTax := 0 c.PaymentInProgress = true c.Processing = true @@ -438,7 +437,7 @@ func (c *CartGrain) HandleMessage(message *Message, isReplay bool) (*FrameWithPa total := int(item.Price) * item.Quantity taxAmount := GetTaxAmount(total, item.Tax) totalTax += taxAmount - orderLines = append(orderLines, &klarna.Line{ + orderLines = append(orderLines, &Line{ Type: "physical", Reference: item.Sku, Name: item.Name, @@ -448,10 +447,10 @@ func (c *CartGrain) HandleMessage(message *Message, isReplay bool) (*FrameWithPa QuantityUnit: "st", TotalAmount: total, TotalTaxAmount: taxAmount, - ImageURL: item.Image, + ImageURL: fmt.Sprintf("https://www.elgiganten.se%s", item.Image), }) } - order := klarna.CheckoutOrder{ + order := CheckoutOrder{ PurchaseCountry: "SE", PurchaseCurrency: "SEK", Locale: "sv-se", @@ -459,7 +458,7 @@ func (c *CartGrain) HandleMessage(message *Message, isReplay bool) (*FrameWithPa OrderTaxAmount: totalTax, OrderLines: orderLines, MerchantReference1: c.Id.String(), - MerchantURLS: &klarna.CheckoutMerchantURLS{ + MerchantURLS: &CheckoutMerchantURLS{ Terms: msg.Terms, Checkout: msg.Checkout, Confirmation: msg.Confirmation, diff --git a/go.mod b/go.mod index 953d726..59e5e67 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,10 @@ module git.tornberg.me/go-cart-actor go 1.24.2 require ( - github.com/Flaconi/go-klarna v0.0.0-20230216165926-e2f708c721d9 github.com/matst80/slask-finder v0.0.0-20250418094723-2eb7d6615761 github.com/prometheus/client_golang v1.22.0 github.com/rabbitmq/amqp091-go v1.10.0 github.com/yudhasubki/netpool v0.0.0-20230717065341-3c1353ca328e - golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 google.golang.org/protobuf v1.36.6 k8s.io/api v0.32.3 k8s.io/apimachinery v0.32.3 @@ -51,6 +49,7 @@ require ( golang.org/x/term v0.31.0 // indirect golang.org/x/text v0.24.0 // indirect golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.32.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 8793f42..e4aea59 100644 --- a/go.sum +++ b/go.sum @@ -109,8 +109,6 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= diff --git a/klarna-client.go b/klarna-client.go new file mode 100644 index 0000000..614f2b5 --- /dev/null +++ b/klarna-client.go @@ -0,0 +1,104 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +type KlarnaClient struct { + Url string + UserName string + Password string + client *http.Client +} + +func NewKlarnaClient(url, userName, password string) *KlarnaClient { + return &KlarnaClient{ + Url: url, + UserName: userName, + Password: password, + client: &http.Client{}, + } +} + +const ( + KlarnaPlaygroundUrl = "https://api.playground.klarna.com" +) + +func (k *KlarnaClient) GetOrder(orderId string) (*CheckoutOrder, error) { + req, err := http.NewRequest("GET", k.Url+"/checkout/v3/orders/"+orderId, nil) + if err != nil { + return nil, err + } + req.Header.Add("Content-Type", "application/json") + req.SetBasicAuth(k.UserName, k.Password) + + res, err := k.client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + var klarnaOrderResponse CheckoutOrder + err = json.NewDecoder(res.Body).Decode(&klarnaOrderResponse) + if err != nil { + return nil, err + } + return &klarnaOrderResponse, nil +} + +func (k *KlarnaClient) CreateOrder(reader io.Reader) (*CheckoutOrder, error) { + //bytes.NewReader(reply.Payload) + req, err := http.NewRequest("POST", k.Url+"/checkout/v3/orders", reader) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", "application/json") + req.SetBasicAuth(k.UserName, k.Password) + + res, err := http.DefaultClient.Do(req) + if nil != err { + return nil, err + } + defer res.Body.Close() + var klarnaOrderResponse CheckoutOrder + err = json.NewDecoder(res.Body).Decode(&klarnaOrderResponse) + return &klarnaOrderResponse, err +} + +func (k *KlarnaClient) UpdateOrder(orderId string, reader io.Reader) (*CheckoutOrder, error) { + //bytes.NewReader(reply.Payload) + req, err := http.NewRequest("POST", fmt.Sprintf("%s/checkout/v3/orders/%s", k.Url, orderId), reader) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", "application/json") + req.SetBasicAuth(k.UserName, k.Password) + + res, err := http.DefaultClient.Do(req) + if nil != err { + return nil, err + } + defer res.Body.Close() + var klarnaOrderResponse CheckoutOrder + err = json.NewDecoder(res.Body).Decode(&klarnaOrderResponse) + return &klarnaOrderResponse, err +} + +func (k *KlarnaClient) AbortOrder(orderId string) error { + req, err := http.NewRequest("POST", fmt.Sprintf("%s/checkout/v3/orders/%s/abort", k.Url, orderId), nil) + if err != nil { + return err + } + + req.SetBasicAuth(k.UserName, k.Password) + + _, err = http.DefaultClient.Do(req) + if err != nil { + return err + } + return nil +} diff --git a/klarna-push-type.go b/klarna-push-type.go deleted file mode 100644 index f0a575e..0000000 --- a/klarna-push-type.go +++ /dev/null @@ -1,70 +0,0 @@ -package main - -import "time" - -type KlarnaOrderResponse struct { - OrderID string `json:"order_id"` - Status string `json:"status"` - PurchaseCountry string `json:"purchase_country"` - PurchaseCurrency string `json:"purchase_currency"` - Locale string `json:"locale"` - BillingAddress struct { - GivenName string `json:"given_name"` - FamilyName string `json:"family_name"` - Email string `json:"email"` - StreetAddress string `json:"street_address"` - PostalCode string `json:"postal_code"` - City string `json:"city"` - Phone string `json:"phone"` - Country string `json:"country"` - } `json:"billing_address"` - Customer struct { - DateOfBirth string `json:"date_of_birth"` - Type string `json:"type"` - Gender string `json:"gender"` - } `json:"customer"` - ShippingAddress struct { - GivenName string `json:"given_name"` - FamilyName string `json:"family_name"` - Email string `json:"email"` - StreetAddress string `json:"street_address"` - PostalCode string `json:"postal_code"` - City string `json:"city"` - Phone string `json:"phone"` - Country string `json:"country"` - } `json:"shipping_address"` - OrderAmount int `json:"order_amount"` - OrderTaxAmount int `json:"order_tax_amount"` - OrderLines []struct { - Type string `json:"type"` - Reference string `json:"reference"` - Name string `json:"name"` - Quantity int `json:"quantity"` - QuantityUnit string `json:"quantity_unit"` - UnitPrice int `json:"unit_price"` - TaxRate int `json:"tax_rate"` - TotalAmount int `json:"total_amount"` - TotalDiscountAmount int `json:"total_discount_amount"` - TotalTaxAmount int `json:"total_tax_amount"` - ImageURL string `json:"image_url"` - } `json:"order_lines"` - MerchantUrls struct { - Terms string `json:"terms"` - Checkout string `json:"checkout"` - Confirmation string `json:"confirmation"` - Push string `json:"push"` - } `json:"merchant_urls"` - MerchantReference1 string `json:"merchant_reference1"` - HTMLSnippet string `json:"html_snippet"` - StartedAt time.Time `json:"started_at"` - CompletedAt time.Time `json:"completed_at"` - LastModifiedAt time.Time `json:"last_modified_at"` - Options struct { - AllowSeparateShippingAddress bool `json:"allow_separate_shipping_address"` - DateOfBirthMandatory bool `json:"date_of_birth_mandatory"` - RequireValidateCallbackSuccess bool `json:"require_validate_callback_success"` - } `json:"options"` - ExternalPaymentMethods []interface{} `json:"external_payment_methods"` - ExternalCheckouts []interface{} `json:"external_checkouts"` - PaymentTypeAllowsIncrease bool `json:"payment_type_allows_increase"` -} diff --git a/klarna-types.go b/klarna-types.go new file mode 100644 index 0000000..b04e75c --- /dev/null +++ b/klarna-types.go @@ -0,0 +1,167 @@ +package main + +type ( + LineType string + + // CheckoutOrder type is the request structure to create a new order from the Checkout API + CheckoutOrder struct { + ID string `json:"order_id,omitempty"` + PurchaseCountry string `json:"purchase_country"` + PurchaseCurrency string `json:"purchase_currency"` + Locale string `json:"locale"` + Status string `json:"status,omitempty"` + BillingAddress *Address `json:"billing_address,omitempty"` + ShippingAddress *Address `json:"shipping_address,omitempty"` + OrderAmount int `json:"order_amount"` + OrderTaxAmount int `json:"order_tax_amount"` + OrderLines []*Line `json:"order_lines"` + Customer *CheckoutCustomer `json:"customer,omitempty"` + MerchantURLS *CheckoutMerchantURLS `json:"merchant_urls"` + HTMLSnippet string `json:"html_snippet,omitempty"` + MerchantReference1 string `json:"merchant_reference1,omitempty"` + MerchantReference2 string `json:"merchant_reference2,omitempty"` + StartedAt string `json:"started_at,omitempty"` + CompletedAt string `json:"completed_at,omitempty"` + LastModifiedAt string `json:"last_modified_at,omitempty"` + Options *CheckoutOptions `json:"options,omitempty"` + Attachment *Attachment `json:"attachment,omitempty"` + ExternalPaymentMethods []*PaymentProvider `json:"external_payment_methods,omitempty"` + ExternalCheckouts []*PaymentProvider `json:"external_checkouts,omitempty"` + ShippingCountries []string `json:"shipping_countries,omitempty"` + ShippingOptions []*ShippingOption `json:"shipping_options,omitempty"` + MerchantData string `json:"merchant_data,omitempty"` + GUI *GUI `json:"gui,omitempty"` + MerchantRequested *AdditionalCheckBox `json:"merchant_requested,omitempty"` + SelectedShippingOption *ShippingOption `json:"selected_shipping_option,omitempty"` + } + + // GUI type wraps the GUI options + GUI struct { + Options []string `json:"options,omitempty"` + } + + // ShippingOption type is part of the CheckoutOrder structure, represent the shipping options field + ShippingOption struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Promo string `json:"promo,omitempty"` + Price int `json:"price"` + TaxAmount int `json:"tax_amount"` + TaxRate int `json:"tax_rate"` + Preselected bool `json:"preselected,omitempty"` + ShippingMethod string `json:"shipping_method,omitempty"` + } + + // PaymentProvider type is part of the CheckoutOrder structure, represent the ExternalPaymentMethods and + // ExternalCheckouts field + PaymentProvider struct { + Name string `json:"name"` + RedirectURL string `json:"redirect_url"` + ImageURL string `json:"image_url,omitempty"` + Fee int `json:"fee,omitempty"` + Description string `json:"description,omitempty"` + Countries []string `json:"countries,omitempty"` + } + + Attachment struct { + ContentType string `json:"content_type"` + Body string `json:"body"` + } + + CheckoutOptions struct { + AcquiringChannel string `json:"acquiring_channel,omitempty"` + AllowSeparateShippingAddress bool `json:"allow_separate_shipping_address,omitempty"` + ColorButton string `json:"color_button,omitempty"` + ColorButtonText string `json:"color_button_text,omitempty"` + ColorCheckbox string `json:"color_checkbox,omitempty"` + ColorCheckboxCheckmark string `json:"color_checkbox_checkmark,omitempty"` + ColorHeader string `json:"color_header,omitempty"` + ColorLink string `json:"color_link,omitempty"` + DateOfBirthMandatory bool `json:"date_of_birth_mandatory,omitempty"` + ShippingDetails string `json:"shipping_details,omitempty"` + TitleMandatory bool `json:"title_mandatory,omitempty"` + AdditionalCheckbox *AdditionalCheckBox `json:"additional_checkbox"` + RadiusBorder string `json:"radius_border,omitempty"` + ShowSubtotalDetail bool `json:"show_subtotal_detail,omitempty"` + RequireValidateCallbackSuccess bool `json:"require_validate_callback_success,omitempty"` + AllowGlobalBillingCountries bool `json:"allow_global_billing_countries,omitempty"` + } + + AdditionalCheckBox struct { + Text string `json:"text"` + Checked bool `json:"checked"` + Required bool `json:"required"` + } + + CheckoutMerchantURLS struct { + // URL of merchant terms and conditions. Should be different than checkout, confirmation and push URLs. + // (max 2000 characters) + Terms string `json:"terms"` + + // URL of merchant checkout page. Should be different than terms, confirmation and push URLs. + // (max 2000 characters) + Checkout string `json:"checkout"` + + // URL of merchant confirmation page. Should be different than checkout and confirmation URLs. + // (max 2000 characters) + Confirmation string `json:"confirmation"` + + // URL that will be requested when an order is completed. Should be different than checkout and + // confirmation URLs. (max 2000 characters) + Push string `json:"push"` + // URL that will be requested for final merchant validation. (must be https, max 2000 characters) + Validation string `json:"validation,omitempty"` + + // URL for shipping option update. (must be https, max 2000 characters) + ShippingOptionUpdate string `json:"shipping_option_update,omitempty"` + + // URL for shipping, tax and purchase currency updates. Will be called on address changes. + // (must be https, max 2000 characters) + AddressUpdate string `json:"address_update,omitempty"` + + // URL for notifications on pending orders. (max 2000 characters) + Notification string `json:"notification,omitempty"` + + // URL for shipping, tax and purchase currency updates. Will be called on purchase country changes. + // (must be https, max 2000 characters) + CountryChange string `json:"country_change,omitempty"` + } + + CheckoutCustomer struct { + // DateOfBirth in string representation 2006-01-02 + DateOfBirth string `json:"date_of_birth"` + } + + // Address type define the address object (json serializable) being used for the API to represent billing & + // shipping addresses + Address struct { + GivenName string `json:"given_name,omitempty"` + FamilyName string `json:"family_name,omitempty"` + Email string `json:"email,omitempty"` + Title string `json:"title,omitempty"` + StreetAddress string `json:"street_address,omitempty"` + StreetAddress2 string `json:"street_address2,omitempty"` + PostalCode string `json:"postal_code,omitempty"` + City string `json:"city,omitempty"` + Region string `json:"region,omitempty"` + Phone string `json:"phone,omitempty"` + Country string `json:"country,omitempty"` + } + + Line struct { + Type string `json:"type,omitempty"` + Reference string `json:"reference,omitempty"` + Name string `json:"name"` + Quantity int `json:"quantity"` + QuantityUnit string `json:"quantity_unit,omitempty"` + UnitPrice int `json:"unit_price"` + TaxRate int `json:"tax_rate"` + TotalAmount int `json:"total_amount"` + TotalDiscountAmount int `json:"total_discount_amount,omitempty"` + TotalTaxAmount int `json:"total_tax_amount"` + MerchantData string `json:"merchant_data,omitempty"` + ProductURL string `json:"product_url,omitempty"` + ImageURL string `json:"image_url,omitempty"` + } +) diff --git a/main.go b/main.go index 169f526..5baffbc 100644 --- a/main.go +++ b/main.go @@ -148,7 +148,9 @@ func main() { Url: amqpUrl, } - syncedServer := NewPoolServer(syncedPool, fmt.Sprintf("%s, %s", name, podIp)) + klarnaClient := NewKlarnaClient(KlarnaPlaygroundUrl, os.Getenv("KLARNA_API_USERNAME"), os.Getenv("KLARNA_API_PASSWORD")) + + syncedServer := NewPoolServer(syncedPool, fmt.Sprintf("%s, %s", name, podIp), klarnaClient) mux := http.NewServeMux() mux.Handle("/cart/", http.StripPrefix("/cart", syncedServer.Serve())) // only for local @@ -194,7 +196,9 @@ func main() { } orderId := r.URL.Query().Get("order_id") log.Printf("Order confirmation push: %s", orderId) - req, err := http.NewRequest("GET", fmt.Sprintf("https://api.playground.klarna.com/checkout/v3/orders/%s", orderId), nil) + + order, err := klarnaClient.GetOrder(orderId) + if err != nil { log.Printf("Error creating request: %v\n", err) w.WriteHeader(http.StatusInternalServerError) @@ -202,27 +206,7 @@ func main() { return } - req.Header.Add("Content-Type", "application/json") - req.SetBasicAuth(APIUsername, APIPassword) - - res, err := http.DefaultClient.Do(req) - - if nil != err { - log.Printf("Error making request: %v\n", err) - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - defer res.Body.Close() - var klarnaOrderResponse KlarnaOrderResponse - err = json.NewDecoder(res.Body).Decode(&klarnaOrderResponse) - if err != nil { - log.Printf("Error decoding response: %v\n", err) - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) - return - } - orderToSend, err := json.Marshal(klarnaOrderResponse) + orderToSend, err := json.Marshal(order) if err != nil { log.Printf("Error marshaling order: %v\n", err) } else { @@ -242,11 +226,11 @@ func main() { return } } - _, err = syncedServer.pool.Process(ToCartId(klarnaOrderResponse.MerchantReference1), Message{ + _, err = syncedServer.pool.Process(ToCartId(order.MerchantReference1), Message{ Type: OrderCompletedType, Content: &messages.OrderCreated{ - OrderId: klarnaOrderResponse.OrderID, - Status: klarnaOrderResponse.Status, + OrderId: order.ID, + Status: order.Status, }, }) if err != nil { diff --git a/pool-server.go b/pool-server.go index 3253ce6..83e0b37 100644 --- a/pool-server.go +++ b/pool-server.go @@ -5,24 +5,25 @@ import ( "encoding/json" "fmt" "log" + "math/rand" "net/http" - "os" "strconv" "time" messages "git.tornberg.me/go-cart-actor/proto" - "golang.org/x/exp/rand" ) type PoolServer struct { - pod_name string - pool GrainPool + pod_name string + pool GrainPool + klarnaClient *KlarnaClient } -func NewPoolServer(pool GrainPool, pod_name string) *PoolServer { +func NewPoolServer(pool GrainPool, pod_name string, klarnaClient *KlarnaClient) *PoolServer { return &PoolServer{ - pod_name: pod_name, - pool: pool, + pod_name: pod_name, + pool: pool, + klarnaClient: klarnaClient, } } @@ -215,38 +216,23 @@ func (s *PoolServer) HandleAddRequest(w http.ResponseWriter, r *http.Request, id return s.WriteResult(w, reply) } -var ( - APIUsername = os.Getenv("KLARNA_API_USERNAME") - APIPassword = os.Getenv("KLARNA_API_PASSWORD") -) - func (s *PoolServer) HandleConfirmation(w http.ResponseWriter, r *http.Request, id CartId) error { orderId := r.PathValue("orderId") if orderId == "" { return fmt.Errorf("orderId is empty") } - req, err := http.NewRequest("GET", fmt.Sprintf("https://api.playground.klarna.com/checkout/v3/orders/%s", orderId), nil) + order, err := s.klarnaClient.GetOrder(orderId) + if err != nil { return err } - req.Header.Add("Content-Type", "application/json") - req.SetBasicAuth(APIUsername, APIPassword) - - res, err := http.DefaultClient.Do(req) - if nil != err { - return err - } - buf := new(bytes.Buffer) - buf.ReadFrom(res.Body) w.Header().Set("Content-Type", "application/json") w.Header().Set("X-Pod-Name", s.pod_name) w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Access-Control-Allow-Origin", "*") - w.WriteHeader(res.StatusCode) - - w.Write(buf.Bytes()) - return nil + w.WriteHeader(http.StatusOK) + return json.NewEncoder(w).Encode(order) } func (s *PoolServer) HandleCheckout(w http.ResponseWriter, r *http.Request, id CartId) error { @@ -267,29 +253,19 @@ func (s *PoolServer) HandleCheckout(w http.ResponseWriter, r *http.Request, id C return s.WriteResult(w, reply) } - req, err := http.NewRequest("POST", "https://api.playground.klarna.com/checkout/v3/orders", bytes.NewReader(reply.Payload)) - if err != nil { - return err - } + order, err := s.klarnaClient.CreateOrder(bytes.NewReader(reply.Payload)) - req.Header.Add("Content-Type", "application/json") - req.SetBasicAuth(APIUsername, APIPassword) - - res, err := http.DefaultClient.Do(req) if nil != err { return err } - buf := new(bytes.Buffer) - buf.ReadFrom(res.Body) w.Header().Set("Content-Type", "application/json") w.Header().Set("X-Pod-Name", s.pod_name) w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Access-Control-Allow-Origin", "*") - w.WriteHeader(res.StatusCode) + w.WriteHeader(http.StatusOK) - w.Write(buf.Bytes()) - return nil + return json.NewEncoder(w).Encode(order) } func NewCartId() CartId {