move more code
This commit is contained in:
@@ -6,7 +6,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -205,9 +204,9 @@ type AddRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *PoolServer) GetReservationTime(item *messages.AddItem) time.Duration {
|
func (s *PoolServer) GetReservationTime(item *messages.AddItem) time.Duration {
|
||||||
|
// TODO: Implement reservation time calculation, nil don't require reservation
|
||||||
return time.Minute * 15
|
return time.Minute * 15
|
||||||
//return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PoolServer) AddSkuRequestHandler(w http.ResponseWriter, r *http.Request, id cart.CartId) error {
|
func (s *PoolServer) AddSkuRequestHandler(w http.ResponseWriter, r *http.Request, id cart.CartId) error {
|
||||||
@@ -248,60 +247,6 @@ func (s *PoolServer) AddSkuRequestHandler(w http.ResponseWriter, r *http.Request
|
|||||||
// return json.NewEncoder(w).Encode(order)
|
// return json.NewEncoder(w).Encode(order)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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 (s *PoolServer) HandleCheckout(w http.ResponseWriter, r *http.Request, id CartId) error {
|
// func (s *PoolServer) HandleCheckout(w http.ResponseWriter, r *http.Request, id CartId) error {
|
||||||
// klarnaOrder, err := s.CreateOrUpdateCheckout(r.Host, id)
|
// klarnaOrder, err := s.CreateOrUpdateCheckout(r.Host, id)
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
@@ -315,52 +260,6 @@ func getClientIp(r *http.Request) string {
|
|||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
|
|
||||||
func CookieCartIdHandler(fn func(cartId cart.CartId, w http.ResponseWriter, r *http.Request) error) func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
|
|
||||||
var id cart.CartId
|
|
||||||
cookie, err := r.Cookie("cartid")
|
|
||||||
if err != nil || cookie.Value == "" {
|
|
||||||
id = cart.MustNewCartId()
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: "cartid",
|
|
||||||
Value: id.String(),
|
|
||||||
Secure: r.TLS != nil,
|
|
||||||
HttpOnly: true,
|
|
||||||
Path: "/",
|
|
||||||
Expires: time.Now().AddDate(0, 0, 14),
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
})
|
|
||||||
w.Header().Set("Set-Cart-Id", id.String())
|
|
||||||
} else {
|
|
||||||
parsed, ok := cart.ParseCartId(cookie.Value)
|
|
||||||
if !ok {
|
|
||||||
id = cart.MustNewCartId()
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: "cartid",
|
|
||||||
Value: id.String(),
|
|
||||||
Secure: r.TLS != nil,
|
|
||||||
HttpOnly: true,
|
|
||||||
Path: "/",
|
|
||||||
Expires: time.Now().AddDate(0, 0, 14),
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
})
|
|
||||||
w.Header().Set("Set-Cart-Id", id.String())
|
|
||||||
} else {
|
|
||||||
id = parsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removed leftover legacy block after CookieCartIdHandler (obsolete code referencing cid/legacy)
|
// Removed leftover legacy block after CookieCartIdHandler (obsolete code referencing cid/legacy)
|
||||||
|
|
||||||
func (s *PoolServer) RemoveCartCookie(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
|
func (s *PoolServer) RemoveCartCookie(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
|
||||||
@@ -378,34 +277,6 @@ func (s *PoolServer) RemoveCartCookie(w http.ResponseWriter, r *http.Request, ca
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CartIdHandler(fn func(cartId cart.CartId, w http.ResponseWriter, r *http.Request) error) func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var id cart.CartId
|
|
||||||
raw := r.PathValue("id")
|
|
||||||
// If no id supplied, generate a new one
|
|
||||||
if raw == "" {
|
|
||||||
id := cart.MustNewCartId()
|
|
||||||
w.Header().Set("Set-Cart-Id", id.String())
|
|
||||||
} else {
|
|
||||||
// Parse base62 cart id
|
|
||||||
if parsedId, ok := cart.ParseCartId(raw); !ok {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
w.Write([]byte("cart id is invalid"))
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
id = 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 *PoolServer) ProxyHandler(fn func(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error) func(cartId cart.CartId, w http.ResponseWriter, r *http.Request) error {
|
func (s *PoolServer) ProxyHandler(fn func(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error) func(cartId cart.CartId, w http.ResponseWriter, r *http.Request) error {
|
||||||
return func(cartId cart.CartId, w http.ResponseWriter, r *http.Request) error {
|
return func(cartId cart.CartId, w http.ResponseWriter, r *http.Request) error {
|
||||||
if ownerHost, ok := s.OwnerHost(uint64(cartId)); ok {
|
if ownerHost, ok := s.OwnerHost(uint64(cartId)); ok {
|
||||||
@@ -433,12 +304,9 @@ func (s *PoolServer) ProxyHandler(fn func(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
tracer = otel.Tracer(name)
|
tracer = otel.Tracer(name)
|
||||||
hmacKey = os.Getenv("ADYEN_HMAC")
|
|
||||||
meter = otel.Meter(name)
|
meter = otel.Meter(name)
|
||||||
logger = otelslog.NewLogger(name)
|
logger = otelslog.NewLogger(name)
|
||||||
proxyCalls metric.Int64Counter
|
proxyCalls metric.Int64Counter
|
||||||
|
|
||||||
// rollCnt metric.Int64Counter
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
138
cmd/cart/utils.go
Normal file
138
cmd/cart/utils.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
|
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 CookieCartIdHandler(fn func(cartId cart.CartId, w http.ResponseWriter, r *http.Request) error) func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
var id cart.CartId
|
||||||
|
cookie, err := r.Cookie("cartid")
|
||||||
|
if err != nil || cookie.Value == "" {
|
||||||
|
id = cart.MustNewCartId()
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "cartid",
|
||||||
|
Value: id.String(),
|
||||||
|
Secure: r.TLS != nil,
|
||||||
|
HttpOnly: true,
|
||||||
|
Path: "/",
|
||||||
|
Expires: time.Now().AddDate(0, 0, 14),
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
})
|
||||||
|
w.Header().Set("Set-Cart-Id", id.String())
|
||||||
|
} else {
|
||||||
|
parsed, ok := cart.ParseCartId(cookie.Value)
|
||||||
|
if !ok {
|
||||||
|
id = cart.MustNewCartId()
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "cartid",
|
||||||
|
Value: id.String(),
|
||||||
|
Secure: r.TLS != nil,
|
||||||
|
HttpOnly: true,
|
||||||
|
Path: "/",
|
||||||
|
Expires: time.Now().AddDate(0, 0, 14),
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
})
|
||||||
|
w.Header().Set("Set-Cart-Id", id.String())
|
||||||
|
} else {
|
||||||
|
id = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 CartIdHandler(fn func(cartId cart.CartId, w http.ResponseWriter, r *http.Request) error) func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var id cart.CartId
|
||||||
|
raw := r.PathValue("id")
|
||||||
|
// If no id supplied, generate a new one
|
||||||
|
if raw == "" {
|
||||||
|
id := cart.MustNewCartId()
|
||||||
|
w.Header().Set("Set-Cart-Id", id.String())
|
||||||
|
} else {
|
||||||
|
// Parse base62 cart id
|
||||||
|
if parsedId, ok := cart.ParseCartId(raw); !ok {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Write([]byte("cart id is invalid"))
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
id = 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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
220
cmd/checkout/adyen-handlers.go
Normal file
220
cmd/checkout/adyen-handlers.go
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/proxy"
|
||||||
|
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||||
|
adyenCheckout "github.com/adyen/adyen-go-api-library/v21/src/checkout"
|
||||||
|
"github.com/adyen/adyen-go-api-library/v21/src/common"
|
||||||
|
"github.com/adyen/adyen-go-api-library/v21/src/hmacvalidator"
|
||||||
|
"github.com/adyen/adyen-go-api-library/v21/src/webhook"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionRequest struct {
|
||||||
|
SessionId string `json:"sessionId"`
|
||||||
|
SessionResult string `json:"sessionResult"`
|
||||||
|
SessionData string `json:"sessionData,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) AdyenSessionHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
|
||||||
|
|
||||||
|
grain, err := s.Get(r.Context(), uint64(cartId))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
meta := GetCheckoutMetaFromRequest(r)
|
||||||
|
sessionData, err := BuildAdyenCheckoutSession(grain, meta)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
service := s.adyenClient.Checkout()
|
||||||
|
req := service.PaymentsApi.SessionsInput().CreateCheckoutSessionRequest(*sessionData)
|
||||||
|
res, _, err := service.PaymentsApi.Sessions(r.Context(), req)
|
||||||
|
// apply checkout started
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.WriteResult(w, res)
|
||||||
|
} else {
|
||||||
|
payload := &SessionRequest{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
service := s.adyenClient.Checkout()
|
||||||
|
req := service.PaymentsApi.GetResultOfPaymentSessionInput(payload.SessionId).SessionResult(payload.SessionResult)
|
||||||
|
res, _, err := service.PaymentsApi.GetResultOfPaymentSession(r.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.WriteResult(w, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) AdyenHookHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var notificationRequest webhook.Webhook
|
||||||
|
service := s.adyenClient.Checkout()
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(¬ificationRequest); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cartHostMap := make(map[actor.Host][]webhook.NotificationItem)
|
||||||
|
for _, notificationItem := range *notificationRequest.NotificationItems {
|
||||||
|
item := notificationItem.NotificationRequestItem
|
||||||
|
log.Printf("Recieved notification event code: %s, %+v", item.EventCode, item)
|
||||||
|
|
||||||
|
isValid := hmacvalidator.ValidateHmac(item, hmacKey)
|
||||||
|
if !isValid {
|
||||||
|
log.Printf("notification hmac not valid %s, %v", item.EventCode, item)
|
||||||
|
http.Error(w, "Invalid HMAC", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
switch item.EventCode {
|
||||||
|
case "CAPTURE":
|
||||||
|
log.Printf("Capture status: %v", item.Success)
|
||||||
|
// dataBytes, err := json.Marshal(item)
|
||||||
|
// if err != nil {
|
||||||
|
// log.Printf("error marshaling item: %v", err)
|
||||||
|
// http.Error(w, "Error marshaling item", http.StatusInternalServerError)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//s.ApplyAnywhere(r.Context(),0, &messages.PaymentEvent{PaymentId: item.PspReference, Success: item.Success, Name: item.EventCode, Data: &pbany.Any{Value: dataBytes}})
|
||||||
|
case "AUTHORISATION":
|
||||||
|
|
||||||
|
cartId, ok := cart.ParseCartId(item.MerchantReference)
|
||||||
|
if !ok {
|
||||||
|
log.Printf("invalid cart id %s", item.MerchantReference)
|
||||||
|
http.Error(w, "Invalid cart id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//s.Apply()
|
||||||
|
|
||||||
|
if host, ok := s.OwnerHost(uint64(cartId)); ok {
|
||||||
|
cartHostMap[host] = append(cartHostMap[host], notificationItem)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
grain, err := s.Get(r.Context(), uint64(cartId))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error getting cart: %v", err)
|
||||||
|
http.Error(w, "Cart not found", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
meta := GetCheckoutMetaFromRequest(r)
|
||||||
|
pspReference := item.PspReference
|
||||||
|
uid := uuid.New().String()
|
||||||
|
ref := uuid.New().String()
|
||||||
|
req := service.ModificationsApi.CaptureAuthorisedPaymentInput(pspReference).IdempotencyKey(uid).PaymentCaptureRequest(adyenCheckout.PaymentCaptureRequest{
|
||||||
|
Amount: adyenCheckout.Amount{
|
||||||
|
Currency: meta.Currency,
|
||||||
|
Value: grain.CartTotalPrice.IncVat,
|
||||||
|
},
|
||||||
|
MerchantAccount: "ElgigantenECOM",
|
||||||
|
Reference: &ref,
|
||||||
|
})
|
||||||
|
res, _, err := service.ModificationsApi.CaptureAuthorisedPayment(r.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error capturing payment: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Payment captured successfully: %+v", res)
|
||||||
|
s.Apply(r.Context(), uint64(cartId), &messages.OrderCreated{
|
||||||
|
OrderId: res.PaymentPspReference,
|
||||||
|
Status: item.EventCode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.Printf("Unknown event code: %s", item.EventCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var failed bool = false
|
||||||
|
var lastMock *proxy.MockResponseWriter
|
||||||
|
for host, items := range cartHostMap {
|
||||||
|
notificationRequest.NotificationItems = &items
|
||||||
|
bodyBytes, err := json.Marshal(notificationRequest)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error marshaling notification: %+v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
customBody := bytes.NewReader(bodyBytes)
|
||||||
|
mockW := proxy.NewMockResponseWriter()
|
||||||
|
handled, err := host.Proxy(0, mockW, r, customBody)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("proxy failed for %s: %+v", host.Name(), err)
|
||||||
|
failed = true
|
||||||
|
lastMock = mockW
|
||||||
|
} else if handled {
|
||||||
|
log.Printf("notification proxied to %s", host.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if failed {
|
||||||
|
w.WriteHeader(lastMock.StatusCode)
|
||||||
|
w.Write(lastMock.Body.Bytes())
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) AdyenReturnHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("Redirect received")
|
||||||
|
|
||||||
|
service := s.adyenClient.Checkout()
|
||||||
|
|
||||||
|
req := service.PaymentsApi.GetResultOfPaymentSessionInput(r.URL.Query().Get("sessionId"))
|
||||||
|
|
||||||
|
res, httpRes, err := service.PaymentsApi.GetResultOfPaymentSession(r.Context(), req)
|
||||||
|
log.Printf("got payment session %+v", res)
|
||||||
|
|
||||||
|
dreq := service.PaymentsApi.PaymentsDetailsInput()
|
||||||
|
dreq = dreq.PaymentDetailsRequest(adyenCheckout.PaymentDetailsRequest{
|
||||||
|
Details: adyenCheckout.PaymentCompletionDetails{
|
||||||
|
RedirectResult: common.PtrString(r.URL.Query().Get("redirectResult")),
|
||||||
|
Payload: common.PtrString(r.URL.Query().Get("payload")),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
dres, httpRes, err := service.PaymentsApi.PaymentsDetails(r.Context(), dreq)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("Payment details response: %+v", dres)
|
||||||
|
|
||||||
|
if !common.IsNil(dres.PspReference) && *dres.PspReference != "" {
|
||||||
|
var redirectURL string
|
||||||
|
// Conditionally handle different result codes for the shopper
|
||||||
|
switch *dres.ResultCode {
|
||||||
|
case "Authorised":
|
||||||
|
redirectURL = "/result/success"
|
||||||
|
case "Pending", "Received":
|
||||||
|
redirectURL = "/result/pending"
|
||||||
|
case "Refused":
|
||||||
|
redirectURL = "/result/failed"
|
||||||
|
default:
|
||||||
|
reason := ""
|
||||||
|
if dres.RefusalReason != nil {
|
||||||
|
reason = *dres.RefusalReason
|
||||||
|
} else {
|
||||||
|
reason = *dres.ResultCode
|
||||||
|
}
|
||||||
|
log.Printf("Payment failed: %s", reason)
|
||||||
|
redirectURL = fmt.Sprintf("/result/error?reason=%s", url.QueryEscape(reason))
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(httpRes.StatusCode)
|
||||||
|
json.NewEncoder(w).Encode(httpRes.Status)
|
||||||
|
}
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"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"
|
|
||||||
|
|
||||||
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
|
||||||
amqp "github.com/rabbitmq/amqp091-go"
|
|
||||||
)
|
|
||||||
|
|
||||||
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>
|
|
||||||
`
|
|
||||||
|
|
||||||
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 *App) getGrainFromOrder(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.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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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.CartState.Items)
|
|
||||||
err = inventoryService.ReserveInventory(r.Context(), inventoryRequests...)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logger.WarnContext(r.Context(), "placeorder inventory reservation failed")
|
|
||||||
w.WriteHeader(http.StatusNotAcceptable)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
a.pool.Apply(r.Context(), uint64(grain.Id), &messages.InventoryReserved{
|
|
||||||
Id: grain.Id.String(),
|
|
||||||
Status: "success",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply ConfirmationViewed mutation
|
|
||||||
cartId, ok := cart.ParseCartId(order.MerchantReference1)
|
|
||||||
if ok {
|
|
||||||
a.pool.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)
|
|
||||||
})
|
|
||||||
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.CartState.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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
168
cmd/checkout/klarna-handlers.go
Normal file
168
cmd/checkout/klarna-handlers.go
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = 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>
|
||||||
|
`
|
||||||
|
|
||||||
|
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.Get(ctx, uint64(cartId))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get cart grain: %w", err)
|
||||||
|
}
|
||||||
|
return grain, nil
|
||||||
|
}
|
||||||
@@ -107,21 +107,13 @@ func main() {
|
|||||||
cartClient := NewCartClient(cartInternalUrl)
|
cartClient := NewCartClient(cartInternalUrl)
|
||||||
|
|
||||||
syncedServer := NewCheckoutPoolServer(pool, fmt.Sprintf("%s, %s", name, podIp), klarnaClient, cartClient, adyenClient)
|
syncedServer := NewCheckoutPoolServer(pool, fmt.Sprintf("%s, %s", name, podIp), klarnaClient, cartClient, adyenClient)
|
||||||
|
syncedServer.inventoryService = inventoryService
|
||||||
app := &App{
|
|
||||||
pool: pool,
|
|
||||||
server: syncedServer,
|
|
||||||
klarnaClient: klarnaClient,
|
|
||||||
cartClient: cartClient,
|
|
||||||
}
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
debugMux := http.NewServeMux()
|
debugMux := http.NewServeMux()
|
||||||
|
|
||||||
if amqpUrl == "" {
|
if amqpUrl == "" {
|
||||||
log.Printf("no connection to amqp defined")
|
log.Fatalf("no connection to amqp defined")
|
||||||
} else {
|
|
||||||
app.HandleCheckoutRequests(amqpUrl, mux, inventoryService)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
grpcSrv, err := actor.NewControlServer[*checkout.CheckoutGrain](controlPlaneConfig, pool)
|
grpcSrv, err := actor.NewControlServer[*checkout.CheckoutGrain](controlPlaneConfig, pool)
|
||||||
@@ -143,7 +135,7 @@ func main() {
|
|||||||
syncedServer.Serve(mux)
|
syncedServer.Serve(mux)
|
||||||
|
|
||||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||||
grainCount, capacity := app.pool.LocalUsage()
|
grainCount, capacity := pool.LocalUsage()
|
||||||
if grainCount >= capacity {
|
if grainCount >= capacity {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
w.Write([]byte("grain pool at capacity"))
|
w.Write([]byte("grain pool at capacity"))
|
||||||
|
|||||||
@@ -7,22 +7,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.k6n.net/go-cart-actor/pkg/actor"
|
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||||
"git.k6n.net/go-cart-actor/pkg/cart"
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
"git.k6n.net/go-cart-actor/pkg/checkout"
|
"git.k6n.net/go-cart-actor/pkg/checkout"
|
||||||
"git.k6n.net/go-cart-actor/pkg/proxy"
|
|
||||||
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||||
|
|
||||||
adyen "github.com/adyen/adyen-go-api-library/v21/src/adyen"
|
adyen "github.com/adyen/adyen-go-api-library/v21/src/adyen"
|
||||||
adyenCheckout "github.com/adyen/adyen-go-api-library/v21/src/checkout"
|
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
||||||
"github.com/adyen/adyen-go-api-library/v21/src/common"
|
amqp "github.com/rabbitmq/amqp091-go"
|
||||||
"github.com/adyen/adyen-go-api-library/v21/src/hmacvalidator"
|
|
||||||
"github.com/adyen/adyen-go-api-library/v21/src/webhook"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
@@ -50,10 +45,11 @@ var (
|
|||||||
|
|
||||||
type CheckoutPoolServer struct {
|
type CheckoutPoolServer struct {
|
||||||
actor.GrainPool[*checkout.CheckoutGrain]
|
actor.GrainPool[*checkout.CheckoutGrain]
|
||||||
pod_name string
|
pod_name string
|
||||||
klarnaClient *KlarnaClient
|
klarnaClient *KlarnaClient
|
||||||
adyenClient *adyen.APIClient
|
adyenClient *adyen.APIClient
|
||||||
cartClient *CartClient
|
cartClient *CartClient
|
||||||
|
inventoryService *inventory.RedisInventoryService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCheckoutPoolServer(pool actor.GrainPool[*checkout.CheckoutGrain], pod_name string, klarnaClient *KlarnaClient, cartClient *CartClient, adyenClient *adyen.APIClient) *CheckoutPoolServer {
|
func NewCheckoutPoolServer(pool actor.GrainPool[*checkout.CheckoutGrain], pod_name string, klarnaClient *KlarnaClient, cartClient *CartClient, adyenClient *adyen.APIClient) *CheckoutPoolServer {
|
||||||
@@ -156,56 +152,6 @@ func (s *CheckoutPoolServer) CheckoutHandler(fn func(order *CheckoutOrder, w htt
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
var (
|
||||||
tracer = otel.Tracer(name)
|
tracer = otel.Tracer(name)
|
||||||
hmacKey = os.Getenv("ADYEN_HMAC")
|
hmacKey = os.Getenv("ADYEN_HMAC")
|
||||||
@@ -224,206 +170,6 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionRequest struct {
|
|
||||||
SessionId string `json:"sessionId"`
|
|
||||||
SessionResult string `json:"sessionResult"`
|
|
||||||
SessionData string `json:"sessionData,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CheckoutPoolServer) AdyenSessionHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
|
|
||||||
|
|
||||||
grain, err := s.Get(r.Context(), uint64(cartId))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if r.Method == http.MethodGet {
|
|
||||||
meta := GetCheckoutMetaFromRequest(r)
|
|
||||||
sessionData, err := BuildAdyenCheckoutSession(grain, meta)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
service := s.adyenClient.Checkout()
|
|
||||||
req := service.PaymentsApi.SessionsInput().CreateCheckoutSessionRequest(*sessionData)
|
|
||||||
res, _, err := service.PaymentsApi.Sessions(r.Context(), req)
|
|
||||||
// apply checkout started
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.WriteResult(w, res)
|
|
||||||
} else {
|
|
||||||
payload := &SessionRequest{}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
service := s.adyenClient.Checkout()
|
|
||||||
req := service.PaymentsApi.GetResultOfPaymentSessionInput(payload.SessionId).SessionResult(payload.SessionResult)
|
|
||||||
res, _, err := service.PaymentsApi.GetResultOfPaymentSession(r.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.WriteResult(w, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CheckoutPoolServer) AdyenHookHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var notificationRequest webhook.Webhook
|
|
||||||
service := s.adyenClient.Checkout()
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(¬ificationRequest); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cartHostMap := make(map[actor.Host][]webhook.NotificationItem)
|
|
||||||
for _, notificationItem := range *notificationRequest.NotificationItems {
|
|
||||||
item := notificationItem.NotificationRequestItem
|
|
||||||
log.Printf("Recieved notification event code: %s, %+v", item.EventCode, item)
|
|
||||||
|
|
||||||
isValid := hmacvalidator.ValidateHmac(item, hmacKey)
|
|
||||||
if !isValid {
|
|
||||||
log.Printf("notification hmac not valid %s, %v", item.EventCode, item)
|
|
||||||
http.Error(w, "Invalid HMAC", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
switch item.EventCode {
|
|
||||||
case "CAPTURE":
|
|
||||||
log.Printf("Capture status: %v", item.Success)
|
|
||||||
// dataBytes, err := json.Marshal(item)
|
|
||||||
// if err != nil {
|
|
||||||
// log.Printf("error marshaling item: %v", err)
|
|
||||||
// http.Error(w, "Error marshaling item", http.StatusInternalServerError)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
//s.ApplyAnywhere(r.Context(),0, &messages.PaymentEvent{PaymentId: item.PspReference, Success: item.Success, Name: item.EventCode, Data: &pbany.Any{Value: dataBytes}})
|
|
||||||
case "AUTHORISATION":
|
|
||||||
|
|
||||||
cartId, ok := cart.ParseCartId(item.MerchantReference)
|
|
||||||
if !ok {
|
|
||||||
log.Printf("invalid cart id %s", item.MerchantReference)
|
|
||||||
http.Error(w, "Invalid cart id", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
//s.Apply()
|
|
||||||
|
|
||||||
if host, ok := s.OwnerHost(uint64(cartId)); ok {
|
|
||||||
cartHostMap[host] = append(cartHostMap[host], notificationItem)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
grain, err := s.Get(r.Context(), uint64(cartId))
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error getting cart: %v", err)
|
|
||||||
http.Error(w, "Cart not found", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
meta := GetCheckoutMetaFromRequest(r)
|
|
||||||
pspReference := item.PspReference
|
|
||||||
uid := uuid.New().String()
|
|
||||||
ref := uuid.New().String()
|
|
||||||
req := service.ModificationsApi.CaptureAuthorisedPaymentInput(pspReference).IdempotencyKey(uid).PaymentCaptureRequest(adyenCheckout.PaymentCaptureRequest{
|
|
||||||
Amount: adyenCheckout.Amount{
|
|
||||||
Currency: meta.Currency,
|
|
||||||
Value: grain.CartTotalPrice.IncVat,
|
|
||||||
},
|
|
||||||
MerchantAccount: "ElgigantenECOM",
|
|
||||||
Reference: &ref,
|
|
||||||
})
|
|
||||||
res, _, err := service.ModificationsApi.CaptureAuthorisedPayment(r.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error capturing payment: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Printf("Payment captured successfully: %+v", res)
|
|
||||||
s.Apply(r.Context(), uint64(cartId), &messages.OrderCreated{
|
|
||||||
OrderId: res.PaymentPspReference,
|
|
||||||
Status: item.EventCode,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
log.Printf("Unknown event code: %s", item.EventCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var failed bool = false
|
|
||||||
var lastMock *proxy.MockResponseWriter
|
|
||||||
for host, items := range cartHostMap {
|
|
||||||
notificationRequest.NotificationItems = &items
|
|
||||||
bodyBytes, err := json.Marshal(notificationRequest)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error marshaling notification: %+v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
customBody := bytes.NewReader(bodyBytes)
|
|
||||||
mockW := proxy.NewMockResponseWriter()
|
|
||||||
handled, err := host.Proxy(0, mockW, r, customBody)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("proxy failed for %s: %+v", host.Name(), err)
|
|
||||||
failed = true
|
|
||||||
lastMock = mockW
|
|
||||||
} else if handled {
|
|
||||||
log.Printf("notification proxied to %s", host.Name())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if failed {
|
|
||||||
w.WriteHeader(lastMock.StatusCode)
|
|
||||||
w.Write(lastMock.Body.Bytes())
|
|
||||||
} else {
|
|
||||||
w.WriteHeader(http.StatusAccepted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CheckoutPoolServer) AdyenReturnHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Println("Redirect received")
|
|
||||||
|
|
||||||
service := s.adyenClient.Checkout()
|
|
||||||
|
|
||||||
req := service.PaymentsApi.GetResultOfPaymentSessionInput(r.URL.Query().Get("sessionId"))
|
|
||||||
|
|
||||||
res, httpRes, err := service.PaymentsApi.GetResultOfPaymentSession(r.Context(), req)
|
|
||||||
log.Printf("got payment session %+v", res)
|
|
||||||
|
|
||||||
dreq := service.PaymentsApi.PaymentsDetailsInput()
|
|
||||||
dreq = dreq.PaymentDetailsRequest(adyenCheckout.PaymentDetailsRequest{
|
|
||||||
Details: adyenCheckout.PaymentCompletionDetails{
|
|
||||||
RedirectResult: common.PtrString(r.URL.Query().Get("redirectResult")),
|
|
||||||
Payload: common.PtrString(r.URL.Query().Get("payload")),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
dres, httpRes, err := service.PaymentsApi.PaymentsDetails(r.Context(), dreq)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Printf("Payment details response: %+v", dres)
|
|
||||||
|
|
||||||
if !common.IsNil(dres.PspReference) && *dres.PspReference != "" {
|
|
||||||
var redirectURL string
|
|
||||||
// Conditionally handle different result codes for the shopper
|
|
||||||
switch *dres.ResultCode {
|
|
||||||
case "Authorised":
|
|
||||||
redirectURL = "/result/success"
|
|
||||||
case "Pending", "Received":
|
|
||||||
redirectURL = "/result/pending"
|
|
||||||
case "Refused":
|
|
||||||
redirectURL = "/result/failed"
|
|
||||||
default:
|
|
||||||
reason := ""
|
|
||||||
if dres.RefusalReason != nil {
|
|
||||||
reason = *dres.RefusalReason
|
|
||||||
} else {
|
|
||||||
reason = *dres.ResultCode
|
|
||||||
}
|
|
||||||
log.Printf("Payment failed: %s", reason)
|
|
||||||
redirectURL = fmt.Sprintf("/result/error?reason=%s", url.QueryEscape(reason))
|
|
||||||
}
|
|
||||||
http.Redirect(w, r, redirectURL, http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(httpRes.StatusCode)
|
|
||||||
json.NewEncoder(w).Encode(httpRes.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CheckoutPoolServer) Serve(mux *http.ServeMux) {
|
func (s *CheckoutPoolServer) Serve(mux *http.ServeMux) {
|
||||||
handleFunc := func(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) {
|
handleFunc := func(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) {
|
||||||
attr := attribute.String("http.route", pattern)
|
attr := attribute.String("http.route", pattern)
|
||||||
@@ -438,9 +184,63 @@ func (s *CheckoutPoolServer) Serve(mux *http.ServeMux) {
|
|||||||
handlerFunc(w, r)
|
handlerFunc(w, r)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
handleFunc("/payment/adyen/session", s.AdyenSessionHandler)
|
||||||
|
handleFunc("/payment/adyen/push", s.AdyenHookHandler)
|
||||||
|
handleFunc("/payment/adyen/return", s.AdyenReturnHandler)
|
||||||
|
//handleFunc("/payment/adyen/cancel", s.AdyenCancelHandler)
|
||||||
|
|
||||||
handleFunc("/adyen_hook", s.AdyenHookHandler)
|
handleFunc("/payment/klarna/validate", s.KlarnaValidationHandler)
|
||||||
handleFunc("/adyen-return", s.AdyenReturnHandler)
|
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()
|
||||||
|
|
||||||
|
mux.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
|
||||||
|
}))
|
||||||
|
|
||||||
|
mux.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 mutation
|
||||||
|
cartId, ok := cart.ParseCartId(order.MerchantReference1)
|
||||||
|
if ok {
|
||||||
|
a.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)
|
||||||
|
})
|
||||||
|
|
||||||
handleFunc("GET /checkout", s.CheckoutHandler(func(order *CheckoutOrder, w http.ResponseWriter) error {
|
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("Content-Type", "text/html; charset=utf-8")
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/checkout"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getOriginalHost(r *http.Request) string {
|
func getOriginalHost(r *http.Request) string {
|
||||||
@@ -44,3 +51,65 @@ func getCountryFromHost(host string) string {
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *CheckoutPoolServer) reserveInventory(ctx context.Context, grain *checkout.CheckoutGrain) error {
|
||||||
|
if a.inventoryService != nil {
|
||||||
|
inventoryRequests := getInventoryRequests(grain.CartState.Items)
|
||||||
|
_, err := a.inventoryService.ReservationCheck(ctx, inventoryRequests...)
|
||||||
|
if err != nil {
|
||||||
|
logger.WarnContext(ctx, "placeorder inventory check failed")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -95,14 +95,18 @@ type MutationHandler interface {
|
|||||||
type RegisteredMutation[V any, T proto.Message] struct {
|
type RegisteredMutation[V any, T proto.Message] struct {
|
||||||
name string
|
name string
|
||||||
handler func(*V, T) error
|
handler func(*V, T) error
|
||||||
create func() T
|
create func() proto.Message
|
||||||
msgType reflect.Type
|
msgType reflect.Type
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMutation[V any, T proto.Message](handler func(*V, T) error, create func() T) *RegisteredMutation[V, T] {
|
func NewMutation[V any, T proto.Message](handler func(*V, T) error) *RegisteredMutation[V, T] {
|
||||||
// Derive the name and message type from a concrete instance produced by create().
|
// Derive the name and message type from a concrete instance produced by create().
|
||||||
// This avoids relying on reflect.TypeFor (which can yield unexpected results in some toolchains)
|
// This avoids relying on reflect.TypeFor (which can yield unexpected results in some toolchains)
|
||||||
// and ensures we always peel off the pointer layer for proto messages.
|
// and ensures we always peel off the pointer layer for proto messages.
|
||||||
|
create := func() proto.Message {
|
||||||
|
m := new(T)
|
||||||
|
return *m
|
||||||
|
}
|
||||||
instance := create()
|
instance := create()
|
||||||
rt := reflect.TypeOf(instance)
|
rt := reflect.TypeOf(instance)
|
||||||
if rt.Kind() == reflect.Ptr {
|
if rt.Kind() == reflect.Ptr {
|
||||||
|
|||||||
@@ -6,33 +6,32 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.k6n.net/go-cart-actor/pkg/messages"
|
cart_messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||||
)
|
)
|
||||||
|
|
||||||
type cartState struct {
|
type cartState struct {
|
||||||
calls int
|
calls int
|
||||||
lastAdded *messages.AddItem
|
lastAdded *cart_messages.AddItem
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegisteredMutationBasics(t *testing.T) {
|
func TestRegisteredMutationBasics(t *testing.T) {
|
||||||
reg := NewMutationRegistry().(*ProtoMutationRegistry)
|
reg := NewMutationRegistry().(*ProtoMutationRegistry)
|
||||||
|
|
||||||
addItemMutation := NewMutation(
|
addItemMutation := NewMutation(
|
||||||
func(state *cartState, msg *messages.AddItem) error {
|
func(state *cartState, msg *cart_messages.AddItem) error {
|
||||||
state.calls++
|
state.calls++
|
||||||
// copy to avoid external mutation side-effects (not strictly necessary for the test)
|
// copy to avoid external mutation side-effects (not strictly necessary for the test)
|
||||||
cp := msg
|
cp := msg
|
||||||
state.lastAdded = cp
|
state.lastAdded = cp
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
func() *messages.AddItem { return &messages.AddItem{} },
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Sanity check on mutation metadata
|
// Sanity check on mutation metadata
|
||||||
if addItemMutation.Name() != "AddItem" {
|
if addItemMutation.Name() != "AddItem" {
|
||||||
t.Fatalf("expected mutation Name() == AddItem, got %s", addItemMutation.Name())
|
t.Fatalf("expected mutation Name() == AddItem, got %s", addItemMutation.Name())
|
||||||
}
|
}
|
||||||
if got, want := addItemMutation.Type(), reflect.TypeOf(messages.AddItem{}); got != want {
|
if got, want := addItemMutation.Type(), reflect.TypeOf(cart_messages.AddItem{}); got != want {
|
||||||
t.Fatalf("expected Type() == %v, got %v", want, got)
|
t.Fatalf("expected Type() == %v, got %v", want, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,18 +45,18 @@ func TestRegisteredMutationBasics(t *testing.T) {
|
|||||||
|
|
||||||
// RegisteredMutationTypes: membership (order not guaranteed)
|
// RegisteredMutationTypes: membership (order not guaranteed)
|
||||||
types := reg.RegisteredMutationTypes()
|
types := reg.RegisteredMutationTypes()
|
||||||
if !slices.Contains(types, reflect.TypeOf(messages.AddItem{})) {
|
if !slices.Contains(types, reflect.TypeOf(cart_messages.AddItem{})) {
|
||||||
t.Fatalf("RegisteredMutationTypes missing AddItem type, got %v", types)
|
t.Fatalf("RegisteredMutationTypes missing AddItem type, got %v", types)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTypeName should resolve for a pointer instance
|
// GetTypeName should resolve for a pointer instance
|
||||||
name, ok := reg.GetTypeName(&messages.AddItem{})
|
name, ok := reg.GetTypeName(&cart_messages.AddItem{})
|
||||||
if !ok || name != "AddItem" {
|
if !ok || name != "AddItem" {
|
||||||
t.Fatalf("GetTypeName returned (%q,%v), expected (AddItem,true)", name, ok)
|
t.Fatalf("GetTypeName returned (%q,%v), expected (AddItem,true)", name, ok)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTypeName should fail for unregistered type
|
// GetTypeName should fail for unregistered type
|
||||||
if name, ok := reg.GetTypeName(&messages.RemoveItem{}); ok || name != "" {
|
if name, ok := reg.GetTypeName(&cart_messages.RemoveItem{}); ok || name != "" {
|
||||||
t.Fatalf("expected GetTypeName to fail for unregistered message, got (%q,%v)", name, ok)
|
t.Fatalf("expected GetTypeName to fail for unregistered message, got (%q,%v)", name, ok)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +65,7 @@ func TestRegisteredMutationBasics(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("Create failed for registered mutation")
|
t.Fatalf("Create failed for registered mutation")
|
||||||
}
|
}
|
||||||
if _, isAddItem := msg.(*messages.AddItem); !isAddItem {
|
if _, isAddItem := msg.(*cart_messages.AddItem); !isAddItem {
|
||||||
t.Fatalf("Create returned wrong concrete type: %T", msg)
|
t.Fatalf("Create returned wrong concrete type: %T", msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +76,7 @@ func TestRegisteredMutationBasics(t *testing.T) {
|
|||||||
|
|
||||||
// Apply happy path
|
// Apply happy path
|
||||||
state := &cartState{}
|
state := &cartState{}
|
||||||
add := &messages.AddItem{ItemId: 42, Quantity: 3, Sku: "ABC"}
|
add := &cart_messages.AddItem{ItemId: 42, Quantity: 3, Sku: "ABC"}
|
||||||
if _, err := reg.Apply(context.Background(), state, add); err != nil {
|
if _, err := reg.Apply(context.Background(), state, add); err != nil {
|
||||||
t.Fatalf("Apply returned error: %v", err)
|
t.Fatalf("Apply returned error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -99,7 +98,7 @@ func TestRegisteredMutationBasics(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply unregistered message
|
// Apply unregistered message
|
||||||
_, err := reg.Apply(context.Background(), state, &messages.RemoveItem{})
|
_, err := reg.Apply(context.Background(), state, &cart_messages.RemoveItem{})
|
||||||
if err != ErrMutationNotRegistered {
|
if err != ErrMutationNotRegistered {
|
||||||
t.Fatalf("expected ErrMutationNotRegistered, got %v", err)
|
t.Fatalf("expected ErrMutationNotRegistered, got %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.k6n.net/go-cart-actor/pkg/actor"
|
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||||
messages "git.k6n.net/go-cart-actor/proto/cart"
|
|
||||||
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -57,43 +56,28 @@ func (c *CartMutationContext) ReleaseItem(ctx context.Context, cartId CartId, sk
|
|||||||
return c.reservationService.ReleaseForCart(ctx, inventory.SKU(sku), l, inventory.CartID(cartId.String()))
|
return c.reservationService.ReleaseForCart(ctx, inventory.SKU(sku), l, inventory.CartID(cartId.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Create[T any]() func() *T {
|
||||||
|
return func() *T {
|
||||||
|
return new(T)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewCartMultationRegistry(context *CartMutationContext) actor.MutationRegistry {
|
func NewCartMultationRegistry(context *CartMutationContext) actor.MutationRegistry {
|
||||||
|
|
||||||
reg := actor.NewMutationRegistry()
|
reg := actor.NewMutationRegistry()
|
||||||
reg.RegisterMutations(
|
reg.RegisterMutations(
|
||||||
actor.NewMutation(context.AddItem, func() *messages.AddItem {
|
actor.NewMutation(context.AddItem),
|
||||||
return &messages.AddItem{}
|
actor.NewMutation(context.ChangeQuantity),
|
||||||
}),
|
actor.NewMutation(context.RemoveItem),
|
||||||
actor.NewMutation(context.ChangeQuantity, func() *messages.ChangeQuantity {
|
actor.NewMutation(ClearCart),
|
||||||
return &messages.ChangeQuantity{}
|
actor.NewMutation(AddVoucher),
|
||||||
}),
|
actor.NewMutation(RemoveVoucher),
|
||||||
actor.NewMutation(context.RemoveItem, func() *messages.RemoveItem {
|
actor.NewMutation(UpsertSubscriptionDetails),
|
||||||
return &messages.RemoveItem{}
|
actor.NewMutation(SetUserId),
|
||||||
}),
|
actor.NewMutation(LineItemMarking),
|
||||||
actor.NewMutation(ClearCart, func() *messages.ClearCartRequest {
|
actor.NewMutation(RemoveLineItemMarking),
|
||||||
return &messages.ClearCartRequest{}
|
actor.NewMutation(SubscriptionAdded),
|
||||||
}),
|
// actor.NewMutation(SubscriptionRemoved),
|
||||||
actor.NewMutation(AddVoucher, func() *messages.AddVoucher {
|
|
||||||
return &messages.AddVoucher{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(RemoveVoucher, func() *messages.RemoveVoucher {
|
|
||||||
return &messages.RemoveVoucher{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(UpsertSubscriptionDetails, func() *messages.UpsertSubscriptionDetails {
|
|
||||||
return &messages.UpsertSubscriptionDetails{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(SetUserId, func() *messages.SetUserId {
|
|
||||||
return &messages.SetUserId{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(LineItemMarking, func() *messages.LineItemMarking {
|
|
||||||
return &messages.LineItemMarking{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(RemoveLineItemMarking, func() *messages.RemoveLineItemMarking {
|
|
||||||
return &messages.RemoveLineItemMarking{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(SubscriptionAdded, func() *messages.SubscriptionAdded {
|
|
||||||
return &messages.SubscriptionAdded{}
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
return reg
|
return reg
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package checkout
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"git.k6n.net/go-cart-actor/pkg/actor"
|
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||||
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type CheckoutMutationContext struct {
|
type CheckoutMutationContext struct {
|
||||||
@@ -16,20 +15,17 @@ func NewCheckoutMutationContext() *CheckoutMutationContext {
|
|||||||
func NewCheckoutMutationRegistry(ctx *CheckoutMutationContext) actor.MutationRegistry {
|
func NewCheckoutMutationRegistry(ctx *CheckoutMutationContext) actor.MutationRegistry {
|
||||||
reg := actor.NewMutationRegistry()
|
reg := actor.NewMutationRegistry()
|
||||||
reg.RegisterMutations(
|
reg.RegisterMutations(
|
||||||
actor.NewMutation(HandleInitializeCheckout, func() *messages.InitializeCheckout { return &messages.InitializeCheckout{} }),
|
actor.NewMutation(HandleInitializeCheckout),
|
||||||
actor.NewMutation(HandlePaymentStarted, func() *messages.PaymentStarted { return &messages.PaymentStarted{} }),
|
actor.NewMutation(HandlePaymentStarted),
|
||||||
actor.NewMutation(HandlePaymentCompleted, func() *messages.PaymentCompleted { return &messages.PaymentCompleted{} }),
|
actor.NewMutation(HandlePaymentCompleted),
|
||||||
actor.NewMutation(HandlePaymentDeclined, func() *messages.PaymentDeclined { return &messages.PaymentDeclined{} }),
|
actor.NewMutation(HandlePaymentDeclined),
|
||||||
actor.NewMutation(HandlePaymentEvent, func() *messages.PaymentEvent { return &messages.PaymentEvent{} }),
|
actor.NewMutation(HandlePaymentEvent),
|
||||||
actor.NewMutation(HandleConfirmationViewed, func() *messages.ConfirmationViewed { return &messages.ConfirmationViewed{} }),
|
actor.NewMutation(HandleConfirmationViewed),
|
||||||
//actor.NewMutation(HandleCreateCheckoutOrder, func() *messages.CreateCheckoutOrder { return &messages.CreateCheckoutOrder{} }),
|
actor.NewMutation(HandleOrderCreated),
|
||||||
actor.NewMutation(HandleOrderCreated, func() *messages.OrderCreated { return &messages.OrderCreated{} }),
|
actor.NewMutation(HandleInventoryReserved),
|
||||||
actor.NewMutation(HandleInventoryReserved, func() *messages.InventoryReserved { return &messages.InventoryReserved{} }),
|
actor.NewMutation(HandleSetDelivery),
|
||||||
actor.NewMutation(HandleSetDelivery, func() *messages.SetDelivery { return &messages.SetDelivery{} }),
|
actor.NewMutation(HandleSetPickupPoint),
|
||||||
actor.NewMutation(HandleSetPickupPoint, func() *messages.SetPickupPoint { return &messages.SetPickupPoint{} }),
|
actor.NewMutation(HandleRemoveDelivery),
|
||||||
actor.NewMutation(HandleRemoveDelivery, func() *messages.RemoveDelivery { return &messages.RemoveDelivery{} }),
|
|
||||||
// actor.NewMutation(HandleAddGiftcard, func() *messages.AddGiftcard { return &messages.AddGiftcard{} }),
|
|
||||||
// actor.NewMutation(HandleRemoveGiftcard, func() *messages.RemoveGiftcard { return &messages.RemoveGiftcard{} }),
|
|
||||||
)
|
)
|
||||||
return reg
|
return reg
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user