package main import ( "bytes" "encoding/json" "errors" "fmt" "log" "net/http" "net/url" "time" "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" "google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/timestamppb" ) 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 getCheckoutIdFromNotificationItem(item webhook.NotificationRequestItem) (uint64, error) { cartId, ok := cart.ParseCartId(item.MerchantReference) if !ok { return 0, errors.New("invalid cart id") } return uint64(cartId), nil } 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 { // Marshal item data for PaymentEvent dataBytes, err := json.Marshal(item) if err != nil { log.Printf("error marshaling item: %v", err) http.Error(w, "Error marshaling item", http.StatusInternalServerError) return } switch item.EventCode { case "CAPTURE": checkoutId, err := getCheckoutIdFromNotificationItem(item) if err != nil { log.Printf("Could not get checkout id: %v", err) http.Error(w, err.Error(), http.StatusBadRequest) return } // Check if this checkout is owned by another host if host, ok := s.OwnerHost(checkoutId); ok { cartHostMap[host] = append(cartHostMap[host], notificationItem) continue } log.Printf("Capture status: %v", item.Success) isSuccess := item.Success == "true" // Apply payment event for capture if err := s.applyAnywhere(r.Context(), checkoutId, &messages.PaymentEvent{ PaymentId: item.PspReference, Success: isSuccess, Name: item.EventCode, Data: &anypb.Any{Value: dataBytes}, }); err != nil { log.Printf("error applying capture event: %v", err) } // If successful, apply payment completed //if isSuccess { if err := s.applyAnywhere(r.Context(), checkoutId, &messages.PaymentCompleted{ PaymentId: item.PspReference, Status: item.Success, Amount: item.Amount.Value, Currency: item.Amount.Currency, ProcessorReference: &item.PspReference, CompletedAt: timestamppb.New(time.Now()), }); err != nil { http.Error(w, "Message not parsed", http.StatusInternalServerError) return } //} case "AUTHORISATION": isSuccess := item.Success == "true" log.Panicln("Handling auth: %+v", item) checkoutId, err := getCheckoutIdFromNotificationItem(item) if err != nil { log.Printf("Could not get checkout id: %v", err) http.Error(w, err.Error(), http.StatusBadRequest) return } // Apply payment event for authorization if err := s.applyAnywhere(r.Context(), checkoutId, &messages.PaymentEvent{ PaymentId: item.PspReference, Success: isSuccess, Name: item.EventCode, Data: &anypb.Any{Value: dataBytes}, }); err != nil { log.Printf("error applying authorization event: %v", err) } // If successful authorization, trigger capture if isSuccess { grain, err := s.Get(r.Context(), checkoutId) if err != nil { log.Printf("Error getting checkout: %v", err) http.Error(w, "Checkout not found", http.StatusBadRequest) return } meta := GetCheckoutMetaFromRequest(r) pspReference := item.PspReference uid := uuid.New().String() ref := grain.Id.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(), checkoutId, &messages.OrderCreated{ OrderId: res.PaymentPspReference, Status: item.EventCode, }) } } default: log.Printf("Unknown event code: %s", item.EventCode) log.Printf("Item data: %+v", item) //isSuccess := item.Success == "true" // if err := s.applyAnywhere(r.Context(), checkoutId, &messages.PaymentEvent{ // PaymentId: item.PspReference, // Success: isSuccess, // Name: item.EventCode, // Data: &anypb.Any{Value: dataBytes}, // }); err != nil { // log.Printf("error applying payment event: %v", err) // } } } } 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) }