package main import ( "context" "encoding/json" "fmt" "log" "net/http" "time" "git.tornberg.me/go-cart-actor/pkg/actor" "git.tornberg.me/go-cart-actor/pkg/cart" "git.tornberg.me/mats/go-redis-inventory/pkg/inventory" amqp "github.com/rabbitmq/amqp091-go" ) var tpl = ` s10r testing - checkout %s ` func (a *App) getGrainFromOrder(ctx context.Context, order *CheckoutOrder) (*cart.CartGrain, error) { cartId, ok := cart.ParseCartId(order.MerchantReference1) if !ok { return nil, fmt.Errorf("invalid cart id in order reference: %s", order.MerchantReference1) } grain, err := a.pool.Get(ctx, uint64(cartId)) if err != nil { return nil, fmt.Errorf("failed to get cart grain: %w", err) } return grain, nil } func (a *App) HandleCheckoutRequests(amqpUrl string, mux *http.ServeMux, inventoryService inventory.InventoryService) { conn, err := amqp.Dial(amqpUrl) if err != nil { log.Fatalf("failed to connect to RabbitMQ: %v", err) } amqpListener := actor.NewAmqpListener(conn, func(id uint64, msg []actor.ApplyResult) (any, error) { return &CartChangeEvent{ CartId: cart.CartId(id), Mutations: msg, }, nil }) amqpListener.DefineTopics() a.pool.AddListener(amqpListener) orderHandler := NewAmqpOrderHandler(conn) orderHandler.DefineQueue() mux.HandleFunc("/push", func(w http.ResponseWriter, r *http.Request) { log.Printf("Klarna order confirmation push, method: %s", r.Method) if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } orderId := r.URL.Query().Get("order_id") log.Printf("Order confirmation push: %s", orderId) order, err := a.klarnaClient.GetOrder(r.Context(), orderId) if err != nil { log.Printf("Error creating request: %v\n", err) w.WriteHeader(http.StatusInternalServerError) return } grain, err := a.getGrainFromOrder(r.Context(), order) if err != nil { logger.ErrorContext(r.Context(), "Unable to get grain from klarna order", "error", err.Error()) w.WriteHeader(http.StatusInternalServerError) return } if inventoryService != nil { inventoryRequests := getInventoryRequests(grain.Items) err = inventoryService.ReserveInventory(r.Context(), inventoryRequests...) if err != nil { logger.WarnContext(r.Context(), "placeorder inventory reservation failed") w.WriteHeader(http.StatusNotAcceptable) return } } err = confirmOrder(r.Context(), order, orderHandler) if err != nil { log.Printf("Error confirming order: %v\n", err) w.WriteHeader(http.StatusInternalServerError) return } err = triggerOrderCompleted(r.Context(), a.server, order) if err != nil { log.Printf("Error processing cart message: %v\n", err) w.WriteHeader(http.StatusInternalServerError) return } err = a.klarnaClient.AcknowledgeOrder(r.Context(), orderId) if err != nil { log.Printf("Error acknowledging order: %v\n", err) } w.WriteHeader(http.StatusOK) }) mux.HandleFunc("GET /checkout", a.server.CheckoutHandler(func(order *CheckoutOrder, w http.ResponseWriter) error { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Permissions-Policy", "payment=(self \"https://js.stripe.com\" \"https://m.stripe.network\" \"https://js.playground.kustom.co\")") w.WriteHeader(http.StatusOK) _, err := fmt.Fprintf(w, tpl, order.HTMLSnippet) return err })) mux.HandleFunc("GET /confirmation/{order_id}", func(w http.ResponseWriter, r *http.Request) { orderId := r.PathValue("order_id") order, err := a.klarnaClient.GetOrder(r.Context(), orderId) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") if order.Status == "checkout_complete" { http.SetCookie(w, &http.Cookie{ Name: "cartid", Value: "", Path: "/", Secure: true, HttpOnly: true, Expires: time.Unix(0, 0), SameSite: http.SameSiteLaxMode, }) } w.WriteHeader(http.StatusOK) fmt.Fprintf(w, tpl, order.HTMLSnippet) }) mux.HandleFunc("/notification", func(w http.ResponseWriter, r *http.Request) { log.Printf("Klarna order notification, method: %s", r.Method) logger.InfoContext(r.Context(), "Klarna order notification received", "method", r.Method) if r.Method != "POST" { w.WriteHeader(http.StatusMethodNotAllowed) return } order := &CheckoutOrder{} err := json.NewDecoder(r.Body).Decode(order) if err != nil { w.WriteHeader(http.StatusBadRequest) } log.Printf("Klarna order notification: %s", order.ID) logger.InfoContext(r.Context(), "Klarna order notification received", "order_id", order.ID) w.WriteHeader(http.StatusOK) }) mux.HandleFunc("POST /validate", func(w http.ResponseWriter, r *http.Request) { log.Printf("Klarna order validation, method: %s", r.Method) if r.Method != "POST" { w.WriteHeader(http.StatusMethodNotAllowed) return } order := &CheckoutOrder{} err := json.NewDecoder(r.Body).Decode(order) if err != nil { w.WriteHeader(http.StatusBadRequest) } logger.InfoContext(r.Context(), "Klarna order validation received", "order_id", order.ID, "cart_id", order.MerchantReference1) grain, err := a.getGrainFromOrder(r.Context(), order) if err != nil { logger.ErrorContext(r.Context(), "Unable to get grain from klarna order", "error", err.Error()) w.WriteHeader(http.StatusInternalServerError) return } if inventoryService != nil { inventoryRequests := getInventoryRequests(grain.Items) _, err = inventoryService.ReservationCheck(r.Context(), inventoryRequests...) if err != nil { logger.WarnContext(r.Context(), "placeorder inventory check failed") w.WriteHeader(http.StatusNotAcceptable) return } } w.WriteHeader(http.StatusOK) }) }