refactor klarna stuff
Some checks failed
Build and Publish / BuildAndDeploy (push) Successful in 3m42s
Build and Publish / BuildAndDeployAmd64 (push) Has been cancelled

This commit is contained in:
matst80
2025-04-18 20:41:25 +02:00
parent b51fc78dd5
commit 6c2328495b
8 changed files with 302 additions and 145 deletions

View File

@@ -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,

3
go.mod
View File

@@ -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

2
go.sum
View File

@@ -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=

104
klarna-client.go Normal file
View File

@@ -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
}

View File

@@ -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"`
}

167
klarna-types.go Normal file
View File

@@ -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"`
}
)

36
main.go
View File

@@ -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 {

View File

@@ -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 {