add missing code and might be ready for merge
This commit is contained in:
45
cmd/checkout/cart-client.go
Normal file
45
cmd/checkout/cart-client.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
|
"github.com/gogo/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CartClient struct {
|
||||||
|
httpClient *http.Client
|
||||||
|
baseUrl string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCartClient(baseUrl string) *CartClient {
|
||||||
|
return &CartClient{
|
||||||
|
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CartClient) ApplyMutation(cartId cart.CartId, mutation proto.Message) error {
|
||||||
|
url := fmt.Sprintf("%s/internal/cart/%s/mutation", c.baseUrl, cartId.String())
|
||||||
|
data, err := proto.Marshal(mutation)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/protobuf")
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("cart mutation failed: %s", resp.Status)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@@ -9,16 +8,13 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
|
||||||
"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/checkout"
|
"git.k6n.net/go-cart-actor/pkg/checkout"
|
||||||
"git.k6n.net/go-cart-actor/pkg/proxy"
|
"git.k6n.net/go-cart-actor/pkg/proxy"
|
||||||
"github.com/adyen/adyen-go-api-library/v21/src/adyen"
|
"github.com/adyen/adyen-go-api-library/v21/src/adyen"
|
||||||
"github.com/adyen/adyen-go-api-library/v21/src/common"
|
"github.com/adyen/adyen-go-api-library/v21/src/common"
|
||||||
"github.com/gogo/protobuf/proto"
|
|
||||||
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
||||||
"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"
|
||||||
@@ -51,50 +47,6 @@ var redisAddress = os.Getenv("REDIS_ADDRESS")
|
|||||||
var redisPassword = os.Getenv("REDIS_PASSWORD")
|
var redisPassword = os.Getenv("REDIS_PASSWORD")
|
||||||
var cartInternalUrl = os.Getenv("CART_INTERNAL_URL") // e.g., http://cart-service:8081
|
var cartInternalUrl = os.Getenv("CART_INTERNAL_URL") // e.g., http://cart-service:8081
|
||||||
|
|
||||||
func getCountryFromHost(host string) string {
|
|
||||||
if strings.Contains(strings.ToLower(host), "-no") {
|
|
||||||
return "no"
|
|
||||||
}
|
|
||||||
if strings.Contains(strings.ToLower(host), "-se") {
|
|
||||||
return "se"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
type CartClient struct {
|
|
||||||
httpClient *http.Client
|
|
||||||
baseUrl string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCartClient(baseUrl string) *CartClient {
|
|
||||||
return &CartClient{
|
|
||||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
|
||||||
baseUrl: baseUrl,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CartClient) ApplyMutation(cartId cart.CartId, mutation proto.Message) error {
|
|
||||||
url := fmt.Sprintf("%s/internal/cart/%s/mutation", c.baseUrl, cartId.String())
|
|
||||||
data, err := proto.Marshal(mutation)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest("POST", url, bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/protobuf")
|
|
||||||
resp, err := c.httpClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("cart mutation failed: %s", resp.Status)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
controlPlaneConfig := actor.DefaultServerConfig()
|
controlPlaneConfig := actor.DefaultServerConfig()
|
||||||
|
|||||||
@@ -7,15 +7,22 @@ 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/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"
|
||||||
|
|
||||||
"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"
|
||||||
@@ -30,36 +37,6 @@ import (
|
|||||||
"go.opentelemetry.io/otel/trace"
|
"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 (
|
var (
|
||||||
grainMutations = promauto.NewCounter(prometheus.CounterOpts{
|
grainMutations = promauto.NewCounter(prometheus.CounterOpts{
|
||||||
Name: "checkout_grain_mutations_total",
|
Name: "checkout_grain_mutations_total",
|
||||||
@@ -267,8 +244,204 @@ 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) {
|
func (s *CheckoutPoolServer) AdyenHookHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// Similar to cart's, but apply to checkout and then callback to cart
|
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) {
|
||||||
@@ -287,6 +460,7 @@ func (s *CheckoutPoolServer) Serve(mux *http.ServeMux) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleFunc("/adyen_hook", s.AdyenHookHandler)
|
handleFunc("/adyen_hook", s.AdyenHookHandler)
|
||||||
|
handleFunc("/adyen-return", s.AdyenReturnHandler)
|
||||||
|
|
||||||
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")
|
||||||
|
|||||||
46
cmd/checkout/utils.go
Normal file
46
cmd/checkout/utils.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCountryFromHost(host string) string {
|
||||||
|
if strings.Contains(strings.ToLower(host), "-no") {
|
||||||
|
return "no"
|
||||||
|
}
|
||||||
|
if strings.Contains(strings.ToLower(host), "-se") {
|
||||||
|
return "se"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user