package main import ( "context" "encoding/json" "fmt" "log" "net/http" "time" "git.k6n.net/mats/go-cart-actor/pkg/cart" "git.k6n.net/mats/go-cart-actor/pkg/checkout" messages "git.k6n.net/mats/go-cart-actor/proto/checkout" "git.k6n.net/mats/go-redis-inventory/pkg/inventory" "google.golang.org/protobuf/types/known/timestamppb" ) /* * * * s.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 }) */ // func (s *CheckoutPoolServer) KlarnaHtmlCheckoutHandler(w http.ResponseWriter, r *http.Request, checkoutId checkout.CheckoutId) error { // orderId := r.URL.Query().Get("order_id") // var order *CheckoutOrder // var err error // if orderId == "" { // order, err = s.CreateOrUpdateCheckout(r, checkoutId) // if err != nil { // logger.Error("unable to create klarna session", "error", err) // return err // } // // s.ApplyKlarnaPaymentStarted(r.Context(), order, checkoutId) // } // order, err = s.klarnaClient.GetOrder(r.Context(), orderId) // if err != nil { // return err // } // 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 // } // func (s *CheckoutPoolServer) KlarnaSessionHandler(w http.ResponseWriter, r *http.Request, checkoutId checkout.CheckoutId) error { // orderId := r.URL.Query().Get("order_id") // var order *CheckoutOrder // var err error // if orderId == "" { // order, err = s.CreateOrUpdateCheckout(r, checkoutId) // if err != nil { // logger.Error("unable to create klarna session", "error", err) // return err // } // // s.ApplyKlarnaPaymentStarted(r.Context(), order, checkoutId) // } // order, err = s.klarnaClient.GetOrder(r.Context(), orderId) // if err != nil { // return err // } // w.Header().Set("Content-Type", "application/json; charset=utf-8") // return json.NewEncoder(w).Encode(order) // } func (s *CheckoutPoolServer) KlarnaConfirmationHandler(w http.ResponseWriter, r *http.Request) { orderId := r.PathValue("order_id") order, err := s.klarnaClient.GetOrder(r.Context(), orderId) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return } // Apply ConfirmationViewed mutation cartId, ok := cart.ParseCartId(order.MerchantReference1) if ok { s.Apply(r.Context(), uint64(cartId), &messages.ConfirmationViewed{}) } 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) } func (s *CheckoutPoolServer) KlarnaValidationHandler(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) return } logger.InfoContext(r.Context(), "Klarna order validation received", "order_id", order.ID, "cart_id", order.MerchantReference1) grain, err := s.getGrainFromKlarnaOrder(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 } s.reserveInventory(r.Context(), grain) w.WriteHeader(http.StatusOK) } func (s *CheckoutPoolServer) KlarnaNotificationHandler(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) return } log.Printf("Klarna order notification: %s", order.ID) logger.InfoContext(r.Context(), "Klarna order notification received", "order_id", order.ID) w.WriteHeader(http.StatusOK) } func (s *CheckoutPoolServer) KlarnaPushHandler(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 := s.klarnaClient.GetOrder(r.Context(), orderId) if err != nil { log.Printf("Error creating request: %v\n", err) w.WriteHeader(http.StatusInternalServerError) return } grain, err := s.getGrainFromKlarnaOrder(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 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 } s.Apply(r.Context(), uint64(grain.Id), &messages.InventoryReserved{ Id: grain.Id.String(), Status: "success", }) } s.ApplyAnywhere(r.Context(), grain.Id, &messages.PaymentCompleted{ PaymentId: orderId, Status: "completed", ProcessorReference: &order.ID, Amount: int64(order.OrderAmount), Currency: order.PurchaseCurrency, CompletedAt: timestamppb.Now(), }) // ── Create the event-sourced order grain (dual-write with AMQP) ──── if s.orderClient != nil { if _, orderErr := createOrderFromCheckout(r.Context(), s, grain, order, "klarna"); orderErr != nil { // Non-fatal: the checkout grain is updated, the order can be // created on retry (idempotency key guards against duplicates). logger.WarnContext(r.Context(), "from-checkout failed; will retry on next push", "err", orderErr, "checkoutId", grain.Id.String()) } } // ── Dual-write: AMQP order-queue for legacy consumers ─────────────── if s.orderHandler != nil { legacyBody, _ := buildLegacyOrderJSON(grain, order, "klarna", orderId) if pubErr := s.orderHandler.OrderCompleted(legacyBody); pubErr != nil { logger.WarnContext(r.Context(), "order-queue publish failed", "err", pubErr) } } err = s.klarnaClient.AcknowledgeOrder(r.Context(), orderId) if err != nil { log.Printf("Error acknowledging order: %v\n", err) } w.WriteHeader(http.StatusOK) } var tpl = ` s10r testing - checkout %s ` // createOrderFromCheckout builds and sends the from-checkout request to the // order service, creating an event-sourced order grain from the settled checkout. // The idempotency key is "checkout-{checkoutId}" so that retries (Klarna may // push the same order multiple times) are safe. func createOrderFromCheckout(ctx context.Context, s *CheckoutPoolServer, grain *checkout.CheckoutGrain, klarnaOrder *CheckoutOrder, provider string) (*CreateOrderResult, error) { return createOrderFromSettledCheckout(ctx, s, grain, settledPayment{ Provider: provider, Reference: klarnaOrder.ID, Amount: int64(klarnaOrder.OrderAmount), Currency: klarnaOrder.PurchaseCurrency, Country: klarnaOrder.PurchaseCountry, Locale: klarnaOrder.Locale, }) } // buildLegacyOrderJSON builds the go-order-manager compatible JSON for AMQP // dual-write, so existing downstream consumers on the order-queue still receive // order events during the migration period. func buildLegacyOrderJSON(grain *checkout.CheckoutGrain, klarnaOrder *CheckoutOrder, provider string, orderId string) ([]byte, error) { if grain.CartState == nil { return nil, fmt.Errorf("buildLegacyOrderJSON: checkout %s has no cart state", grain.Id) } type legacyLine struct { Reference string `json:"reference"` Name string `json:"name"` Quantity int `json:"quantity"` UnitPrice int `json:"unit_price"` TaxRate int `json:"tax_rate"` TotalAmount int `json:"total_amount"` TotalTaxAmount int `json:"total_tax_amount"` } type legacyOrder struct { ID string `json:"order_id"` PurchaseCountry string `json:"purchase_country"` PurchaseCurrency string `json:"purchase_currency"` Locale string `json:"locale"` OrderAmount int `json:"order_amount"` OrderTaxAmount int `json:"order_tax_amount"` OrderLines []legacyLine `json:"order_lines"` } lines := make([]legacyLine, 0, len(grain.CartState.Items)+len(grain.Deliveries)) for _, it := range grain.CartState.Items { if it == nil { continue } lines = append(lines, legacyLine{ Reference: it.Sku, Name: it.Meta.Name, Quantity: int(it.Quantity), UnitPrice: int(it.Price.IncVat), TaxRate: it.Tax, TotalAmount: int(it.TotalPrice.IncVat), TotalTaxAmount: int(it.TotalPrice.TotalVat()), }) } lo := legacyOrder{ ID: orderId, PurchaseCountry: klarnaOrder.PurchaseCountry, PurchaseCurrency: klarnaOrder.PurchaseCurrency, Locale: klarnaOrder.Locale, OrderAmount: klarnaOrder.OrderAmount, OrderTaxAmount: klarnaOrder.OrderTaxAmount, OrderLines: lines, } return json.Marshal(lo) } func getLocationId(item *cart.CartItem) inventory.LocationID { if item.StoreId == nil || *item.StoreId == "" { return "se" } return inventory.LocationID(*item.StoreId) } func getInventoryRequests(items []*cart.CartItem) []inventory.ReserveRequest { var requests []inventory.ReserveRequest for _, item := range items { if item == nil { continue } requests = append(requests, inventory.ReserveRequest{ InventoryReference: &inventory.InventoryReference{ SKU: inventory.SKU(item.Sku), LocationID: getLocationId(item), }, Quantity: uint32(item.Quantity), }) } return requests } func (a *CheckoutPoolServer) getGrainFromKlarnaOrder(ctx context.Context, order *CheckoutOrder) (*checkout.CheckoutGrain, 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.GetAnywhere(ctx, cartId) if err != nil { return nil, fmt.Errorf("failed to get cart grain: %w", err) } return grain, nil }