181 lines
4.8 KiB
Go
181 lines
4.8 KiB
Go
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)
|
|
}
|
|
}
|