package main import ( "context" "fmt" "log" "net/http" "strings" "time" "git.k6n.net/mats/go-cart-actor/pkg/cart" "git.k6n.net/mats/go-cart-actor/pkg/checkout" "git.k6n.net/mats/platform/inventory" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" ) func getOriginalHost(r *http.Request) string { proxyHost := r.Header.Get("X-Forwarded-Host") if proxyHost != "" { return proxyHost } 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 == "" { ip = r.RemoteAddr } return ip } func getCurrency(country string) string { if country == "no" { return "NOK" } return "SEK" } func getLocale(country string) string { if country == "no" { return "nb-no" } return "sv-se" } // getCountryFromCurrency is the reverse of getCurrency, used by server-to-server // callbacks (e.g. the Adyen webhook) that carry the settled currency but not the // shopper's host/country. func getCountryFromCurrency(currency string) string { if strings.EqualFold(currency, "NOK") { return "no" } return "se" } func getCountryFromHost(host string) string { if strings.Contains(strings.ToLower(host), "-no") { return "no" } if strings.Contains(strings.ToLower(host), "-se") { return "se" } return "" } func (a *CheckoutPoolServer) reserveInventory(ctx context.Context, grain *checkout.CheckoutGrain) error { if a.reservationPolicy == nil { return nil } for _, item := range grain.CartState.Items { if item == nil || !shouldTrackInventory(item) { continue } loc := inventory.LocationID("se") if item.StoreId != nil && *item.StoreId != "" { loc = inventory.LocationID(*item.StoreId) } avail, err := a.reservationPolicy.Available(ctx, inventory.SKU(item.Sku), loc) if err != nil { return fmt.Errorf("inventory check failed for SKU %s: %w", item.Sku, err) } if int64(item.Quantity) > avail { return fmt.Errorf("insufficient inventory for SKU %s: have %d, need %d", item.Sku, avail, item.Quantity) } } return nil } const checkoutCookieName = "checkoutid" func setCheckoutCookie(w http.ResponseWriter, checkoutId checkout.CheckoutId, tls bool) { if checkoutId == 0 { http.SetCookie(w, &http.Cookie{ Name: checkoutCookieName, Value: checkoutId.String(), Secure: tls, HttpOnly: true, Path: "/", Expires: time.Unix(0, 0), SameSite: http.SameSiteLaxMode, }) } else { http.SetCookie(w, &http.Cookie{ Name: checkoutCookieName, Value: checkoutId.String(), Secure: tls, HttpOnly: true, Path: "/", Expires: time.Now().AddDate(0, 0, 14), SameSite: http.SameSiteLaxMode, }) } } func CookieCheckoutIdHandler(fn func(w http.ResponseWriter, r *http.Request, checkoutId checkout.CheckoutId) error) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var id checkout.CheckoutId cookie, err := r.Cookie(checkoutCookieName) if err != nil || cookie.Value == "" { w.WriteHeader(http.StatusNotAcceptable) return } else { parsed, ok := cart.ParseCartId(cookie.Value) if !ok { w.WriteHeader(http.StatusNotAcceptable) return } else { id = parsed } } err = fn(w, r, id) if err != nil { log.Printf("Server error, not remote error: %v\n", err) w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) } } } func (s *CheckoutPoolServer) ProxyHandler(fn func(w http.ResponseWriter, r *http.Request, checkoutId checkout.CheckoutId) error) func(w http.ResponseWriter, r *http.Request, checkoutId checkout.CheckoutId) error { return func(w http.ResponseWriter, r *http.Request, checkoutId checkout.CheckoutId) error { if ownerHost, ok := s.OwnerHost(uint64(checkoutId)); ok { ctx, span := tracer.Start(r.Context(), "proxy") defer span.End() span.SetAttributes(attribute.String("checkoutid", checkoutId.String())) hostAttr := attribute.String("other host", ownerHost.Name()) span.SetAttributes(hostAttr) logger.InfoContext(ctx, "checkout proxyed", "result", ownerHost.Name()) proxyCalls.Add(ctx, 1, metric.WithAttributes(hostAttr)) handled, err := ownerHost.Proxy(uint64(checkoutId), w, r, nil) grainLookups.Inc() if err == nil && handled { return nil } } _, span := tracer.Start(r.Context(), "own") span.SetAttributes(attribute.String("checkoutid", checkoutId.String())) defer span.End() return fn(w, r, checkoutId) } }