273 lines
8.4 KiB
Go
273 lines
8.4 KiB
Go
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/platform/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
|
|
}
|
|
|
|
// Inventory is NOT committed here. Checkout announces a completed sale; the
|
|
// order saga emits order.created and the inventory service reacts (commit +
|
|
// release the cart hold + emit a level crossing), idempotently. This keeps a
|
|
// down inventory service from blocking the revenue path — the payment is
|
|
// already settled at Klarna, so the sale is a fact, not a request. See
|
|
// docs/inventory-shape.md.
|
|
|
|
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 (HTTP with AMQP fallback) ────
|
|
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())
|
|
}
|
|
|
|
err = s.klarnaClient.AcknowledgeOrder(r.Context(), orderId)
|
|
if err != nil {
|
|
log.Printf("Error acknowledging order: %v\n", err)
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
var tpl = `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>s10r testing - checkout</title>
|
|
</head>
|
|
|
|
<body>
|
|
%s
|
|
</body>
|
|
</html>
|
|
`
|
|
|
|
// 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,
|
|
})
|
|
}
|
|
|
|
func getLocationId(item *cart.CartItem) inventory.LocationID {
|
|
if item.StoreId == nil || *item.StoreId == "" {
|
|
return inventory.LocationID("se")
|
|
}
|
|
return inventory.LocationID(*item.StoreId)
|
|
}
|
|
|
|
func shouldTrackInventory(item *cart.CartItem) bool {
|
|
if item.DropShip {
|
|
return false
|
|
}
|
|
if item.InventoryTracked {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
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
|
|
}
|