klarna and checkouts
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
||||||
"git.k6n.net/mats/go-cart-actor/pkg/checkout"
|
"git.k6n.net/mats/go-cart-actor/pkg/checkout"
|
||||||
@@ -11,18 +13,41 @@ import (
|
|||||||
"github.com/adyen/adyen-go-api-library/v21/src/common"
|
"github.com/adyen/adyen-go-api-library/v21/src/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// defaultCallbackBaseURL is where Klarna's server-to-server callbacks
|
||||||
|
// (push/notification/validate) are sent when CHECKOUT_CALLBACK_BASE_URL is unset.
|
||||||
|
// These must be a publicly reachable https origin for the checkout service.
|
||||||
|
const defaultCallbackBaseURL = "https://cart.k6n.net"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// checkoutPublicURL overrides the request-derived site base for the
|
||||||
|
// shopper-facing Klarna URLs (terms/checkout/confirmation). Set this to a
|
||||||
|
// public https origin (e.g. a tunnel or shop-test.tornberg.me) for local
|
||||||
|
// KCO testing, since Klarna rejects non-https merchant_urls. Empty = derive
|
||||||
|
// from the request (X-Forwarded-Host + https).
|
||||||
|
checkoutPublicURL = strings.TrimRight(os.Getenv("CHECKOUT_PUBLIC_URL"), "/")
|
||||||
|
// checkoutCallbackBaseURL is the base for Klarna's server-to-server
|
||||||
|
// callbacks. Defaults to defaultCallbackBaseURL.
|
||||||
|
checkoutCallbackBaseURL = func() string {
|
||||||
|
if v := strings.TrimRight(os.Getenv("CHECKOUT_CALLBACK_BASE_URL"), "/"); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return defaultCallbackBaseURL
|
||||||
|
}()
|
||||||
|
)
|
||||||
|
|
||||||
// CheckoutMeta carries the external / URL metadata required to build a
|
// CheckoutMeta carries the external / URL metadata required to build a
|
||||||
// Klarna CheckoutOrder from a CartGrain snapshot. It deliberately excludes
|
// Klarna CheckoutOrder from a CartGrain snapshot. It deliberately excludes
|
||||||
// any Klarna-specific response fields (HTML snippet, client token, etc.).
|
// any Klarna-specific response fields (HTML snippet, client token, etc.).
|
||||||
type CheckoutMeta struct {
|
type CheckoutMeta struct {
|
||||||
SiteUrl string
|
SiteUrl string
|
||||||
// Terms string
|
// CallbackBaseUrl is the base for Klarna server-to-server callbacks
|
||||||
// Checkout string
|
// (push/notification/validate); may differ from SiteUrl in prod where the
|
||||||
// Confirmation string
|
// checkout service and storefront are on different hosts.
|
||||||
ClientIp string
|
CallbackBaseUrl string
|
||||||
Country string
|
ClientIp string
|
||||||
Currency string // optional override (defaults to "SEK" if empty)
|
Country string
|
||||||
Locale string // optional override (defaults to "sv-se" if empty)
|
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
|
// BuildCheckoutOrderPayload converts the current cart grain + meta information
|
||||||
@@ -113,11 +138,11 @@ func BuildCheckoutOrderPayload(grain *checkout.CheckoutGrain, meta *CheckoutMeta
|
|||||||
MerchantReference1: grain.Id.String(),
|
MerchantReference1: grain.Id.String(),
|
||||||
MerchantURLS: &CheckoutMerchantURLS{
|
MerchantURLS: &CheckoutMerchantURLS{
|
||||||
Terms: fmt.Sprintf("%s/terms", meta.SiteUrl),
|
Terms: fmt.Sprintf("%s/terms", meta.SiteUrl),
|
||||||
Checkout: fmt.Sprintf("%s/checkout?order_id={checkout.order.id}&provider=klarna", meta.SiteUrl),
|
Checkout: fmt.Sprintf("%s/kassa?order_id={checkout.order.id}&provider=klarna", meta.SiteUrl),
|
||||||
Confirmation: fmt.Sprintf("%s/checkout?order_id={checkout.order.id}&provider=klarna", meta.SiteUrl),
|
Confirmation: fmt.Sprintf("%s/kassa?order_id={checkout.order.id}&provider=klarna", meta.SiteUrl),
|
||||||
Notification: "https://cart.k6n.net/payment/klarna/notification",
|
Notification: fmt.Sprintf("%s/payment/klarna/notification", meta.CallbackBaseUrl),
|
||||||
Validation: "https://cart.k6n.net/payment/klarna/validate",
|
Validation: fmt.Sprintf("%s/payment/klarna/validate", meta.CallbackBaseUrl),
|
||||||
Push: "https://cart.k6n.net/payment/klarna/push?order_id={checkout.order.id}",
|
Push: fmt.Sprintf("%s/payment/klarna/push?order_id={checkout.order.id}", meta.CallbackBaseUrl),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,12 +157,17 @@ func BuildCheckoutOrderPayload(grain *checkout.CheckoutGrain, meta *CheckoutMeta
|
|||||||
func GetCheckoutMetaFromRequest(r *http.Request) *CheckoutMeta {
|
func GetCheckoutMetaFromRequest(r *http.Request) *CheckoutMeta {
|
||||||
host := getOriginalHost(r)
|
host := getOriginalHost(r)
|
||||||
country := getCountryFromHost(host)
|
country := getCountryFromHost(host)
|
||||||
|
siteUrl := fmt.Sprintf("%s://%s", getScheme(r), host)
|
||||||
|
if checkoutPublicURL != "" {
|
||||||
|
siteUrl = checkoutPublicURL
|
||||||
|
}
|
||||||
return &CheckoutMeta{
|
return &CheckoutMeta{
|
||||||
ClientIp: getClientIp(r),
|
ClientIp: getClientIp(r),
|
||||||
SiteUrl: fmt.Sprintf("https://%s", host),
|
SiteUrl: siteUrl,
|
||||||
Country: country,
|
CallbackBaseUrl: checkoutCallbackBaseURL,
|
||||||
Currency: getCurrency(country),
|
Country: country,
|
||||||
Locale: getLocale(country),
|
Currency: getCurrency(country),
|
||||||
|
Locale: getLocale(country),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -183,16 +183,20 @@ func (s *CheckoutPoolServer) KlarnaPushHandler(w http.ResponseWriter, r *http.Re
|
|||||||
|
|
||||||
if s.inventoryService != nil {
|
if s.inventoryService != nil {
|
||||||
inventoryRequests := getInventoryRequests(grain.CartState.Items)
|
inventoryRequests := getInventoryRequests(grain.CartState.Items)
|
||||||
err = s.inventoryService.ReserveInventory(r.Context(), inventoryRequests...)
|
invStatus := "success"
|
||||||
|
if err = s.inventoryService.ReserveInventory(r.Context(), inventoryRequests...); err != nil {
|
||||||
if err != nil {
|
// The payment is already settled at Klarna (checkout_complete), so the
|
||||||
logger.WarnContext(r.Context(), "placeorder inventory reservation failed")
|
// order MUST be created. Inventory is best-effort at this point — a
|
||||||
w.WriteHeader(http.StatusNotAcceptable)
|
// reservation failure (unstocked / drop-ship / non-tracked items)
|
||||||
return
|
// must not block order creation. Log it and flag the grain so
|
||||||
|
// fulfillment can reconcile.
|
||||||
|
logger.WarnContext(r.Context(), "klarna push: inventory reservation failed; creating order anyway",
|
||||||
|
"err", err, "checkoutId", grain.Id.String())
|
||||||
|
invStatus = "failed"
|
||||||
}
|
}
|
||||||
s.Apply(r.Context(), uint64(grain.Id), &messages.InventoryReserved{
|
s.Apply(r.Context(), uint64(grain.Id), &messages.InventoryReserved{
|
||||||
Id: grain.Id.String(),
|
Id: grain.Id.String(),
|
||||||
Status: "success",
|
Status: invStatus,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,19 @@ func getOriginalHost(r *http.Request) string {
|
|||||||
return r.Host
|
return r.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getScheme resolves the public scheme for building absolute URLs (e.g. Klarna
|
||||||
|
// merchant_urls). Honors X-Forwarded-Proto set by the edge/dev proxy; defaults
|
||||||
|
// to https so production (TLS terminated at nginx/ingress, no header) is correct.
|
||||||
|
func getScheme(r *http.Request) string {
|
||||||
|
if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
|
||||||
|
return proto
|
||||||
|
}
|
||||||
|
if r.TLS != nil {
|
||||||
|
return "https"
|
||||||
|
}
|
||||||
|
return "https"
|
||||||
|
}
|
||||||
|
|
||||||
func getClientIp(r *http.Request) string {
|
func getClientIp(r *http.Request) string {
|
||||||
ip := r.Header.Get("X-Forwarded-For")
|
ip := r.Header.Get("X-Forwarded-For")
|
||||||
if ip == "" {
|
if ip == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user