package main import ( "bytes" "context" "encoding/json" "fmt" "log" "net/http" "os" "time" "git.k6n.net/go-cart-actor/pkg/actor" "git.k6n.net/go-cart-actor/pkg/cart" "git.k6n.net/go-cart-actor/pkg/checkout" messages "git.k6n.net/go-cart-actor/proto/checkout" adyen "github.com/adyen/adyen-go-api-library/v21/src/adyen" "github.com/matst80/go-redis-inventory/pkg/inventory" amqp "github.com/rabbitmq/amqp091-go" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "go.opentelemetry.io/contrib/bridges/otelslog" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/trace" ) var ( grainMutations = promauto.NewCounter(prometheus.CounterOpts{ Name: "checkout_grain_mutations_total", Help: "The total number of mutations", }) grainLookups = promauto.NewCounter(prometheus.CounterOpts{ Name: "checkout_grain_lookups_total", Help: "The total number of lookups", }) ) type CheckoutPoolServer struct { actor.GrainPool[*checkout.CheckoutGrain] pod_name string klarnaClient *KlarnaClient adyenClient *adyen.APIClient cartClient *CartClient inventoryService *inventory.RedisInventoryService } func NewCheckoutPoolServer(pool actor.GrainPool[*checkout.CheckoutGrain], pod_name string, klarnaClient *KlarnaClient, cartClient *CartClient, adyenClient *adyen.APIClient) *CheckoutPoolServer { srv := &CheckoutPoolServer{ GrainPool: pool, pod_name: pod_name, klarnaClient: klarnaClient, cartClient: cartClient, adyenClient: adyenClient, } return srv } func (s *CheckoutPoolServer) ApplyLocal(ctx context.Context, id checkout.CheckoutId, mutation ...proto.Message) (*actor.MutationResult[*checkout.CheckoutGrain], error) { return s.Apply(ctx, uint64(id), mutation...) } func (s *CheckoutPoolServer) GetCheckoutHandler(w http.ResponseWriter, r *http.Request, id checkout.CheckoutId) error { grain, err := s.Get(r.Context(), uint64(id)) if err != nil { return err } return s.WriteResult(w, grain) } func (s *CheckoutPoolServer) WriteResult(w http.ResponseWriter, result any) error { w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("X-Pod-Name", s.pod_name) if result == nil { w.WriteHeader(http.StatusInternalServerError) return nil } w.WriteHeader(http.StatusOK) enc := json.NewEncoder(w) err := enc.Encode(result) return err } func (s *CheckoutPoolServer) CreateOrUpdateCheckout(r *http.Request, id checkout.CheckoutId) (*CheckoutOrder, error) { // Get cart state from cart service cartGrain, err := s.Get(r.Context(), uint64(id)) if err != nil { return nil, err } meta := GetCheckoutMetaFromRequest(r) payload, _, err := BuildCheckoutOrderPayload(cartGrain, meta) if err != nil { return nil, err } grain, err := s.Get(r.Context(), uint64(id)) if err != nil { return nil, err } if grain.OrderId != nil { return s.klarnaClient.UpdateOrder(r.Context(), *grain.OrderId, bytes.NewReader(payload)) } else { return s.klarnaClient.CreateOrder(r.Context(), bytes.NewReader(payload)) } } func (s *CheckoutPoolServer) ApplyKlarnaPaymentStarted(ctx context.Context, klarnaOrder *CheckoutOrder, id checkout.CheckoutId) (*actor.MutationResult[*checkout.CheckoutGrain], error) { method := "checkout" return s.ApplyLocal(ctx, id, &messages.PaymentStarted{ PaymentId: klarnaOrder.ID, Amount: int64(klarnaOrder.OrderAmount), Currency: klarnaOrder.PurchaseCurrency, Provider: "klarna", Method: &method, StartedAt: timestamppb.New(time.Now()), }) } // func (s *CheckoutPoolServer) CheckoutHandler(fn func(order *CheckoutOrder, w http.ResponseWriter) error) func(w http.ResponseWriter, r *http.Request) { // return CheckoutIdHandler(s.ProxyHandler(func(w http.ResponseWriter, r *http.Request, checkoutId checkout.CheckoutId) error { // orderId := r.URL.Query().Get("order_id") // 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) // return fn(order, w) // } // order, err := s.klarnaClient.GetOrder(r.Context(), orderId) // if err != nil { // return err // } // return fn(order, w) // })) // } var ( tracer = otel.Tracer(name) hmacKey = os.Getenv("ADYEN_HMAC") meter = otel.Meter(name) logger = otelslog.NewLogger(name) proxyCalls metric.Int64Counter ) func init() { var err error proxyCalls, err = meter.Int64Counter("proxy.calls", metric.WithDescription("Number of proxy calls"), metric.WithUnit("{calls}")) if err != nil { panic(err) } } func (s *CheckoutPoolServer) Serve(mux *http.ServeMux) { handleFunc := func(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) { attr := attribute.String("http.route", pattern) mux.HandleFunc(pattern, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { span := trace.SpanFromContext(r.Context()) span.SetName(pattern) span.SetAttributes(attr) labeler, _ := otelhttp.LabelerFromContext(r.Context()) labeler.Add(attr) handlerFunc(w, r) })) } handleFunc("/payment/adyen/session", CheckoutIdHandler(s.AdyenSessionHandler)) handleFunc("/payment/adyen/push", s.AdyenHookHandler) handleFunc("/payment/adyen/return", s.AdyenReturnHandler) //handleFunc("/payment/adyen/cancel", s.AdyenCancelHandler) handleFunc("/payment/klarna/validate", s.KlarnaValidationHandler) handleFunc("/payment/klarna/notification", s.KlarnaNotificationHandler) conn, err := amqp.Dial(amqpUrl) if err != nil { log.Fatalf("failed to connect to RabbitMQ: %v", err) } orderHandler := NewAmqpOrderHandler(conn) orderHandler.DefineQueue() handleFunc("GET /checkout", CheckoutIdHandler(s.ProxyHandler(s.KlarnaHtmlCheckoutHandler))) handleFunc("GET /confirmation/{order_id}", s.KlarnaConfirmationHandler) // handleFunc("GET /checkout", 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 // })) handleFunc("GET /confirmation/{order_id}", func(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 to checkout checkoutId := checkout.CheckoutId(cart.MustNewCartId()) // Need to resolve from order s.Apply(r.Context(), uint64(checkoutId), &messages.ConfirmationViewed{}) // Callback to cart // cartId := cart.CartId(checkoutId) // Assuming same // s.cartClient.ApplyMutation(cartId, &messages.OrderCreated{OrderId: order.ID, Status: order.Status}) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) fmt.Fprintf(w, tpl, order.HTMLSnippet) }) }