package main import ( "encoding/json" "fmt" "net/http" "git.k6n.net/go-cart-actor/pkg/cart" "git.k6n.net/go-cart-actor/pkg/checkout" adyenCheckout "github.com/adyen/adyen-go-api-library/v21/src/checkout" "github.com/adyen/adyen-go-api-library/v21/src/common" ) // CheckoutMeta carries the external / URL metadata required to build a // Klarna CheckoutOrder from a CartGrain snapshot. It deliberately excludes // any Klarna-specific response fields (HTML snippet, client token, etc.). type CheckoutMeta struct { SiteUrl string // Terms string // Checkout string // Confirmation string ClientIp string Country string Currency string // optional override (defaults to "SEK" if empty) Locale string // optional override (defaults to "sv-se" if empty) } // BuildCheckoutOrderPayload converts the current cart grain + meta information // into a CheckoutOrder domain struct and returns its JSON-serialized payload // (to send to Klarna) alongside the structured CheckoutOrder object. // // This function is PURE: it does not perform any network I/O or mutate the // grain. The caller is responsible for: // // 1. Choosing whether to create or update the Klarna order. // 2. Invoking KlarnaClient.CreateOrder / UpdateOrder with the returned payload. // 3. Applying an InitializeCheckout mutation (or equivalent) with the // resulting Klarna order id + status. // // If you later need to support different tax rates per line, you can extend // CartItem / Delivery to expose that data and propagate it here. func BuildCheckoutOrderPayload(grain *checkout.CheckoutGrain, meta *CheckoutMeta) ([]byte, *CheckoutOrder, error) { if grain == nil { return nil, nil, fmt.Errorf("nil grain") } if meta == nil { return nil, nil, fmt.Errorf("nil checkout meta") } currency := meta.Currency if currency == "" { currency = "SEK" } locale := meta.Locale if locale == "" { locale = "sv-se" } country := meta.Country if country == "" { country = "SE" // sensible default; adjust if multi-country support changes } lines := make([]*Line, 0, len(grain.CartState.Items)+len(grain.Deliveries)) // Item lines for _, it := range grain.CartState.Items { if it == nil { continue } lines = append(lines, &Line{ Type: "physical", Reference: it.Sku, Name: it.Meta.Name, Quantity: int(it.Quantity), UnitPrice: int(it.Price.IncVat), TaxRate: it.Tax, // TODO: derive if variable tax rates are introduced QuantityUnit: "st", TotalAmount: int(it.TotalPrice.IncVat), TotalTaxAmount: int(it.TotalPrice.TotalVat()), ImageURL: fmt.Sprintf("https://www.elgiganten.se%s", it.Meta.Image), }) } total := cart.NewPrice() total.Add(*grain.CartState.TotalPrice) // Delivery lines for _, d := range grain.Deliveries { if d == nil || d.Price.IncVat <= 0 { continue } //total.Add(d.Price) lines = append(lines, &Line{ Type: "shipping_fee", Reference: d.Provider, Name: "Delivery", Quantity: 1, UnitPrice: int(d.Price.IncVat), TaxRate: 2500, QuantityUnit: "st", TotalAmount: int(d.Price.IncVat), TotalTaxAmount: int(d.Price.TotalVat()), }) } order := &CheckoutOrder{ PurchaseCountry: country, PurchaseCurrency: currency, Locale: locale, OrderAmount: int(total.IncVat), OrderTaxAmount: int(total.TotalVat()), OrderLines: lines, MerchantReference1: grain.Id.String(), MerchantURLS: &CheckoutMerchantURLS{ Terms: fmt.Sprintf("%s/terms", meta.SiteUrl), Checkout: fmt.Sprintf("%s/checkout?order_id={checkout.order.id}", meta.SiteUrl), Confirmation: fmt.Sprintf("%s/confirmation/{checkout.order.id}", meta.SiteUrl), Notification: "https://cart.k6n.net/payment/klarna/notification", Validation: "https://cart.k6n.net/payment/klarna/validate", Push: "https://cart.k6n.net/payment/klarna/push?order_id={checkout.order.id}", }, } payload, err := json.Marshal(order) if err != nil { return nil, nil, fmt.Errorf("marshal checkout order: %w", err) } return payload, order, nil } func GetCheckoutMetaFromRequest(r *http.Request) *CheckoutMeta { host := getOriginalHost(r) country := getCountryFromHost(host) return &CheckoutMeta{ ClientIp: getClientIp(r), SiteUrl: fmt.Sprintf("https://%s", host), Country: country, Currency: getCurrency(country), Locale: getLocale(country), } } func BuildAdyenCheckoutSession(grain *checkout.CheckoutGrain, meta *CheckoutMeta) (*adyenCheckout.CreateCheckoutSessionRequest, error) { if grain == nil { return nil, fmt.Errorf("nil grain") } if meta == nil { return nil, fmt.Errorf("nil checkout meta") } currency := meta.Currency if currency == "" { currency = "SEK" } country := meta.Country if country == "" { country = "SE" } lineItems := make([]adyenCheckout.LineItem, 0, len(grain.CartState.Items)+len(grain.Deliveries)) // Item lines for _, it := range grain.CartState.Items { if it == nil { continue } lineItems = append(lineItems, adyenCheckout.LineItem{ Quantity: common.PtrInt64(int64(it.Quantity)), AmountIncludingTax: common.PtrInt64(it.TotalPrice.IncVat), Description: common.PtrString(it.Meta.Name), AmountExcludingTax: common.PtrInt64(it.TotalPrice.ValueExVat()), TaxAmount: common.PtrInt64(it.TotalPrice.TotalVat()), TaxPercentage: common.PtrInt64(int64(it.Tax)), }) } total := cart.NewPrice() total.Add(*grain.CartState.TotalPrice) // Delivery lines for _, d := range grain.Deliveries { if d == nil || d.Price.IncVat <= 0 { continue } lineItems = append(lineItems, adyenCheckout.LineItem{ Quantity: common.PtrInt64(1), AmountIncludingTax: common.PtrInt64(d.Price.IncVat), Description: common.PtrString("Delivery"), AmountExcludingTax: common.PtrInt64(d.Price.ValueExVat()), TaxPercentage: common.PtrInt64(25), }) } return &adyenCheckout.CreateCheckoutSessionRequest{ Reference: grain.Id.String(), Amount: adyenCheckout.Amount{ Value: total.IncVat, Currency: currency, }, CountryCode: common.PtrString(country), MerchantAccount: "ElgigantenECOM", Channel: common.PtrString("Web"), ShopperIP: common.PtrString(meta.ClientIp), ReturnUrl: fmt.Sprintf("%s/payment/adyen/return", meta.SiteUrl), LineItems: lineItems, }, nil }