split cart and checkout and checkout vs payments
This commit is contained in:
320
cmd/checkout/pool-server.go
Normal file
320
cmd/checkout/pool-server.go
Normal file
@@ -0,0 +1,320 @@
|
||||
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/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"
|
||||
)
|
||||
|
||||
func getOriginalHost(r *http.Request) string {
|
||||
proxyHost := r.Header.Get("X-Forwarded-Host")
|
||||
if proxyHost != "" {
|
||||
return proxyHost
|
||||
}
|
||||
return r.Host
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *CheckoutPoolServer) getCartGrain(ctx context.Context, cartId cart.CartId, version int) (*cart.CartGrain, error) {
|
||||
// Call cart service to get grain
|
||||
url := fmt.Sprintf("%s/internal/cart/%s/%d", s.cartClient.baseUrl, cartId.String(), version)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := s.cartClient.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to get cart: %s", resp.Status)
|
||||
}
|
||||
var grain cart.CartGrain
|
||||
err = json.NewDecoder(resp.Body).Decode(&grain)
|
||||
return &grain, err
|
||||
}
|
||||
|
||||
func CheckoutIdHandler(fn func(checkoutId checkout.CheckoutId, w http.ResponseWriter, r *http.Request) error) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var id checkout.CheckoutId
|
||||
raw := r.PathValue("id")
|
||||
if raw == "" {
|
||||
id = checkout.CheckoutId(cart.MustNewCartId())
|
||||
w.Header().Set("Set-Checkout-Id", id.String())
|
||||
} else {
|
||||
if parsedId, ok := cart.ParseCartId(raw); !ok {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("checkout id is invalid"))
|
||||
return
|
||||
} else {
|
||||
id = checkout.CheckoutId(parsedId)
|
||||
}
|
||||
}
|
||||
|
||||
err := fn(id, w, r)
|
||||
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(checkoutId checkout.CheckoutId, w http.ResponseWriter, r *http.Request) error {
|
||||
return func(checkoutId checkout.CheckoutId, w http.ResponseWriter, r *http.Request) 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)
|
||||
}
|
||||
}
|
||||
|
||||
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) AdyenHookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Similar to cart's, but apply to checkout and then callback to cart
|
||||
}
|
||||
|
||||
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("/adyen_hook", s.AdyenHookHandler)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user