klarna and checkouts
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/checkout"
|
||||
@@ -11,14 +13,37 @@ import (
|
||||
"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
|
||||
// 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
|
||||
// CallbackBaseUrl is the base for Klarna server-to-server callbacks
|
||||
// (push/notification/validate); may differ from SiteUrl in prod where the
|
||||
// checkout service and storefront are on different hosts.
|
||||
CallbackBaseUrl string
|
||||
ClientIp string
|
||||
Country string
|
||||
Currency string // optional override (defaults to "SEK" if empty)
|
||||
@@ -113,11 +138,11 @@ func BuildCheckoutOrderPayload(grain *checkout.CheckoutGrain, meta *CheckoutMeta
|
||||
MerchantReference1: grain.Id.String(),
|
||||
MerchantURLS: &CheckoutMerchantURLS{
|
||||
Terms: fmt.Sprintf("%s/terms", meta.SiteUrl),
|
||||
Checkout: fmt.Sprintf("%s/checkout?order_id={checkout.order.id}&provider=klarna", meta.SiteUrl),
|
||||
Confirmation: fmt.Sprintf("%s/checkout?order_id={checkout.order.id}&provider=klarna", 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}",
|
||||
Checkout: fmt.Sprintf("%s/kassa?order_id={checkout.order.id}&provider=klarna", meta.SiteUrl),
|
||||
Confirmation: fmt.Sprintf("%s/kassa?order_id={checkout.order.id}&provider=klarna", meta.SiteUrl),
|
||||
Notification: fmt.Sprintf("%s/payment/klarna/notification", meta.CallbackBaseUrl),
|
||||
Validation: fmt.Sprintf("%s/payment/klarna/validate", meta.CallbackBaseUrl),
|
||||
Push: fmt.Sprintf("%s/payment/klarna/push?order_id={checkout.order.id}", meta.CallbackBaseUrl),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -132,9 +157,14 @@ func BuildCheckoutOrderPayload(grain *checkout.CheckoutGrain, meta *CheckoutMeta
|
||||
func GetCheckoutMetaFromRequest(r *http.Request) *CheckoutMeta {
|
||||
host := getOriginalHost(r)
|
||||
country := getCountryFromHost(host)
|
||||
siteUrl := fmt.Sprintf("%s://%s", getScheme(r), host)
|
||||
if checkoutPublicURL != "" {
|
||||
siteUrl = checkoutPublicURL
|
||||
}
|
||||
return &CheckoutMeta{
|
||||
ClientIp: getClientIp(r),
|
||||
SiteUrl: fmt.Sprintf("https://%s", host),
|
||||
SiteUrl: siteUrl,
|
||||
CallbackBaseUrl: checkoutCallbackBaseURL,
|
||||
Country: 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 {
|
||||
inventoryRequests := getInventoryRequests(grain.CartState.Items)
|
||||
err = s.inventoryService.ReserveInventory(r.Context(), inventoryRequests...)
|
||||
|
||||
if err != nil {
|
||||
logger.WarnContext(r.Context(), "placeorder inventory reservation failed")
|
||||
w.WriteHeader(http.StatusNotAcceptable)
|
||||
return
|
||||
invStatus := "success"
|
||||
if err = s.inventoryService.ReserveInventory(r.Context(), inventoryRequests...); err != nil {
|
||||
// The payment is already settled at Klarna (checkout_complete), so the
|
||||
// order MUST be created. Inventory is best-effort at this point — a
|
||||
// reservation failure (unstocked / drop-ship / non-tracked items)
|
||||
// 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{
|
||||
Id: grain.Id.String(),
|
||||
Status: "success",
|
||||
Status: invStatus,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,19 @@ func getOriginalHost(r *http.Request) string {
|
||||
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 {
|
||||
ip := r.Header.Get("X-Forwarded-For")
|
||||
if ip == "" {
|
||||
|
||||
Reference in New Issue
Block a user