refactor/checkout #8
@@ -1,6 +1,9 @@
|
|||||||
name: Build and Publish
|
name: Build and Publish
|
||||||
run-name: ${{ gitea.actor }} build 🚀
|
run-name: ${{ gitea.actor }} build 🚀
|
||||||
on: [push]
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
BuildAndDeployAmd64:
|
BuildAndDeployAmd64:
|
||||||
|
|||||||
26
Makefile
26
Makefile
@@ -19,7 +19,10 @@
|
|||||||
|
|
||||||
MODULE_PATH := git.k6n.net/go-cart-actor
|
MODULE_PATH := git.k6n.net/go-cart-actor
|
||||||
PROTO_DIR := proto
|
PROTO_DIR := proto
|
||||||
PROTOS := $(PROTO_DIR)/messages.proto $(PROTO_DIR)/control_plane.proto
|
PROTOS := $(PROTO_DIR)/cart.proto $(PROTO_DIR)/control_plane.proto $(PROTO_DIR)/checkout.proto
|
||||||
|
CART_PROTO_DIR := $(PROTO_DIR)/cart
|
||||||
|
CONTROL_PROTO_DIR := $(PROTO_DIR)/control
|
||||||
|
CHECKOUT_PROTO_DIR := $(PROTO_DIR)/checkout
|
||||||
|
|
||||||
# Allow override: make PROTOC=/path/to/protoc
|
# Allow override: make PROTOC=/path/to/protoc
|
||||||
PROTOC ?= protoc
|
PROTOC ?= protoc
|
||||||
@@ -69,21 +72,30 @@ check_tools:
|
|||||||
protogen: check_tools
|
protogen: check_tools
|
||||||
@echo "$(YELLOW)Generating protobuf code (outputs -> ./proto)...$(RESET)"
|
@echo "$(YELLOW)Generating protobuf code (outputs -> ./proto)...$(RESET)"
|
||||||
$(PROTOC) -I $(PROTO_DIR) \
|
$(PROTOC) -I $(PROTO_DIR) \
|
||||||
--go_out=./pkg/messages --go_opt=paths=source_relative \
|
--go_out=./proto/cart --go_opt=paths=source_relative \
|
||||||
--go-grpc_out=./pkg/messages --go-grpc_opt=paths=source_relative \
|
--go-grpc_out=./proto/cart --go-grpc_opt=paths=source_relative \
|
||||||
$(PROTOS)
|
$(PROTO_DIR)/cart.proto
|
||||||
|
$(PROTOC) -I $(PROTO_DIR) \
|
||||||
|
--go_out=./proto/control --go_opt=paths=source_relative \
|
||||||
|
--go-grpc_out=./proto/control --go-grpc_opt=paths=source_relative \
|
||||||
|
$(PROTO_DIR)/control_plane.proto
|
||||||
|
$(PROTOC) -I $(PROTO_DIR) \
|
||||||
|
--go_out=./proto/checkout --go_opt=paths=source_relative \
|
||||||
|
--go-grpc_out=./proto/checkout --go-grpc_opt=paths=source_relative \
|
||||||
|
$(PROTO_DIR)/checkout.proto
|
||||||
@echo "$(GREEN)Protobuf generation complete.$(RESET)"
|
@echo "$(GREEN)Protobuf generation complete.$(RESET)"
|
||||||
|
|
||||||
clean_proto:
|
clean_proto:
|
||||||
@echo "$(YELLOW)Removing generated protobuf files...$(RESET)"
|
@echo "$(YELLOW)Removing generated protobuf files...$(RESET)"
|
||||||
@rm -f $(PROTO_DIR)/*_grpc.pb.go $(PROTO_DIR)/*.pb.go
|
@rm -f $(PROTO_DIR)/cart/*_grpc.pb.go $(PROTO_DIR)/cart/*.pb.go
|
||||||
@rm -f *.pb.go
|
@rm -f $(PROTO_DIR)/control/*_grpc.pb.go $(PROTO_DIR)/control/*.pb.go
|
||||||
|
@rm -f $(PROTO_DIR)/checkout/*_grpc.pb.go $(PROTO_DIR)/checkout/*.pb.go
|
||||||
@echo "$(GREEN)Clean complete.$(RESET)"
|
@echo "$(GREEN)Clean complete.$(RESET)"
|
||||||
|
|
||||||
verify_proto:
|
verify_proto:
|
||||||
@echo "$(YELLOW)Verifying proto layout...$(RESET)"
|
@echo "$(YELLOW)Verifying proto layout...$(RESET)"
|
||||||
@if ls *.pb.go >/dev/null 2>&1; then \
|
@if ls *.pb.go >/dev/null 2>&1; then \
|
||||||
echo "$(RED)ERROR: Found root-level generated *.pb.go files (should be only under $(PROTO_DIR)/).$(RESET)"; \
|
echo "$(RED)ERROR: Found root-level generated *.pb.go files (should be only under $(PROTO_DIR)/ subdirs).$(RESET)"; \
|
||||||
ls -1 *.pb.go; \
|
ls -1 *.pb.go; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.k6n.net/go-cart-actor/pkg/actor"
|
|
||||||
"git.k6n.net/go-cart-actor/pkg/cart"
|
|
||||||
"git.k6n.net/go-cart-actor/pkg/messages"
|
|
||||||
|
|
||||||
"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 (a *App) getGrainFromOrder(ctx context.Context, order *CheckoutOrder) (*cart.CartGrain, 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
amqpListener := actor.NewAmqpListener(conn, func(id uint64, msg []actor.ApplyResult) (any, error) {
|
|
||||||
return &CartChangeEvent{
|
|
||||||
CartId: cart.CartId(id),
|
|
||||||
Mutations: msg,
|
|
||||||
}, nil
|
|
||||||
})
|
|
||||||
amqpListener.DefineTopics()
|
|
||||||
a.pool.AddListener(amqpListener)
|
|
||||||
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.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.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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"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/discovery"
|
"git.k6n.net/go-cart-actor/pkg/discovery"
|
||||||
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
)
|
)
|
||||||
@@ -24,7 +25,11 @@ func GetDiscovery() discovery.Discovery {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error creating client: %v\n", err)
|
log.Fatalf("Error creating client: %v\n", err)
|
||||||
}
|
}
|
||||||
return discovery.NewK8sDiscovery(client)
|
timeout := int64(30)
|
||||||
|
return discovery.NewK8sDiscovery(client, v1.ListOptions{
|
||||||
|
LabelSelector: "actor-pool=cart",
|
||||||
|
TimeoutSeconds: &timeout,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func UseDiscovery(pool actor.GrainPool[*cart.CartGrain]) {
|
func UseDiscovery(pool actor.GrainPool[*cart.CartGrain]) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
@@ -15,12 +14,9 @@ import (
|
|||||||
|
|
||||||
"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"
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
|
||||||
"git.k6n.net/go-cart-actor/pkg/promotions"
|
"git.k6n.net/go-cart-actor/pkg/promotions"
|
||||||
"git.k6n.net/go-cart-actor/pkg/proxy"
|
"git.k6n.net/go-cart-actor/pkg/proxy"
|
||||||
"git.k6n.net/go-cart-actor/pkg/voucher"
|
"git.k6n.net/go-cart-actor/pkg/voucher"
|
||||||
"github.com/adyen/adyen-go-api-library/v21/src/adyen"
|
|
||||||
"github.com/adyen/adyen-go-api-library/v21/src/common"
|
|
||||||
"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"
|
||||||
@@ -43,7 +39,6 @@ func init() {
|
|||||||
type App struct {
|
type App struct {
|
||||||
pool *actor.SimpleGrainPool[cart.CartGrain]
|
pool *actor.SimpleGrainPool[cart.CartGrain]
|
||||||
server *PoolServer
|
server *PoolServer
|
||||||
klarnaClient *KlarnaClient
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var podIp = os.Getenv("POD_IP")
|
var podIp = os.Getenv("POD_IP")
|
||||||
@@ -195,30 +190,16 @@ func main() {
|
|||||||
log.Fatalf("Error creating cart pool: %v\n", err)
|
log.Fatalf("Error creating cart pool: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
adyenClient := adyen.NewClient(&common.Config{
|
syncedServer := NewPoolServer(pool, fmt.Sprintf("%s, %s", name, podIp), inventoryService, inventoryReservationService)
|
||||||
ApiKey: os.Getenv("ADYEN_API_KEY"),
|
|
||||||
Environment: common.TestEnv,
|
|
||||||
})
|
|
||||||
|
|
||||||
klarnaClient := NewKlarnaClient(KlarnaPlaygroundUrl, os.Getenv("KLARNA_API_USERNAME"), os.Getenv("KLARNA_API_PASSWORD"))
|
|
||||||
|
|
||||||
syncedServer := NewPoolServer(pool, fmt.Sprintf("%s, %s", name, podIp), klarnaClient, inventoryService, inventoryReservationService, adyenClient)
|
|
||||||
|
|
||||||
app := &App{
|
app := &App{
|
||||||
pool: pool,
|
pool: pool,
|
||||||
server: syncedServer,
|
server: syncedServer,
|
||||||
klarnaClient: klarnaClient,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
debugMux := http.NewServeMux()
|
debugMux := http.NewServeMux()
|
||||||
|
|
||||||
if amqpUrl == "" {
|
|
||||||
log.Printf("no connection to amqp defined")
|
|
||||||
} else {
|
|
||||||
app.HandleCheckoutRequests(amqpUrl, mux, inventoryService)
|
|
||||||
}
|
|
||||||
|
|
||||||
grpcSrv, err := actor.NewControlServer[*cart.CartGrain](controlPlaneConfig, pool)
|
grpcSrv, err := actor.NewControlServer[*cart.CartGrain](controlPlaneConfig, pool)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error starting control plane gRPC server: %v\n", err)
|
log.Fatalf("Error starting control plane gRPC server: %v\n", err)
|
||||||
@@ -331,31 +312,3 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func triggerOrderCompleted(ctx context.Context, syncedServer *PoolServer, order *CheckoutOrder) error {
|
|
||||||
mutation := &messages.OrderCreated{
|
|
||||||
OrderId: order.ID,
|
|
||||||
Status: order.Status,
|
|
||||||
}
|
|
||||||
cid, ok := cart.ParseCartId(order.MerchantReference1)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid cart id in order reference: %s", order.MerchantReference1)
|
|
||||||
}
|
|
||||||
_, applyErr := syncedServer.Apply(ctx, uint64(cid), mutation)
|
|
||||||
|
|
||||||
return applyErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func confirmOrder(ctx context.Context, order *CheckoutOrder, orderHandler *AmqpOrderHandler) error {
|
|
||||||
orderToSend, err := json.Marshal(order)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = orderHandler.OrderCompleted(orderToSend)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,29 +1,20 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"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"
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||||
"git.k6n.net/go-cart-actor/pkg/proxy"
|
|
||||||
"git.k6n.net/go-cart-actor/pkg/voucher"
|
"git.k6n.net/go-cart-actor/pkg/voucher"
|
||||||
"github.com/adyen/adyen-go-api-library/v21/src/adyen"
|
|
||||||
"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/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"
|
||||||
@@ -52,20 +43,16 @@ var (
|
|||||||
type PoolServer struct {
|
type PoolServer struct {
|
||||||
actor.GrainPool[*cart.CartGrain]
|
actor.GrainPool[*cart.CartGrain]
|
||||||
pod_name string
|
pod_name string
|
||||||
klarnaClient *KlarnaClient
|
|
||||||
adyenClient *adyen.APIClient
|
|
||||||
inventoryService inventory.InventoryService
|
inventoryService inventory.InventoryService
|
||||||
reservationService inventory.CartReservationService
|
reservationService inventory.CartReservationService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPoolServer(pool actor.GrainPool[*cart.CartGrain], pod_name string, klarnaClient *KlarnaClient, inventoryService inventory.InventoryService, inventoryReservationService inventory.CartReservationService, adyenClient *adyen.APIClient) *PoolServer {
|
func NewPoolServer(pool actor.GrainPool[*cart.CartGrain], pod_name string, inventoryService inventory.InventoryService, inventoryReservationService inventory.CartReservationService) *PoolServer {
|
||||||
srv := &PoolServer{
|
srv := &PoolServer{
|
||||||
GrainPool: pool,
|
GrainPool: pool,
|
||||||
pod_name: pod_name,
|
pod_name: pod_name,
|
||||||
klarnaClient: klarnaClient,
|
|
||||||
inventoryService: inventoryService,
|
inventoryService: inventoryService,
|
||||||
reservationService: inventoryReservationService,
|
reservationService: inventoryReservationService,
|
||||||
adyenClient: adyenClient,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return srv
|
return srv
|
||||||
@@ -130,71 +117,6 @@ func (s *PoolServer) DeleteItemHandler(w http.ResponseWriter, r *http.Request, i
|
|||||||
return s.WriteResult(w, data)
|
return s.WriteResult(w, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SetDeliveryRequest struct {
|
|
||||||
Provider string `json:"provider"`
|
|
||||||
Items []uint32 `json:"items"`
|
|
||||||
PickupPoint *messages.PickupPoint `json:"pickupPoint,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PoolServer) SetDeliveryHandler(w http.ResponseWriter, r *http.Request, id cart.CartId) error {
|
|
||||||
|
|
||||||
delivery := SetDeliveryRequest{}
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&delivery)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
data, err := s.ApplyLocal(r.Context(), id, &messages.SetDelivery{
|
|
||||||
Provider: delivery.Provider,
|
|
||||||
Items: delivery.Items,
|
|
||||||
PickupPoint: delivery.PickupPoint,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.WriteResult(w, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PoolServer) SetPickupPointHandler(w http.ResponseWriter, r *http.Request, id cart.CartId) error {
|
|
||||||
|
|
||||||
deliveryIdString := r.PathValue("deliveryId")
|
|
||||||
deliveryId, err := strconv.ParseInt(deliveryIdString, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
pickupPoint := messages.PickupPoint{}
|
|
||||||
err = json.NewDecoder(r.Body).Decode(&pickupPoint)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
reply, err := s.ApplyLocal(r.Context(), id, &messages.SetPickupPoint{
|
|
||||||
DeliveryId: uint32(deliveryId),
|
|
||||||
Id: pickupPoint.Id,
|
|
||||||
Name: pickupPoint.Name,
|
|
||||||
Address: pickupPoint.Address,
|
|
||||||
City: pickupPoint.City,
|
|
||||||
Zip: pickupPoint.Zip,
|
|
||||||
Country: pickupPoint.Country,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.WriteResult(w, reply)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PoolServer) RemoveDeliveryHandler(w http.ResponseWriter, r *http.Request, id cart.CartId) error {
|
|
||||||
|
|
||||||
deliveryIdString := r.PathValue("deliveryId")
|
|
||||||
deliveryId, err := strconv.Atoi(deliveryIdString)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
reply, err := s.ApplyLocal(r.Context(), id, &messages.RemoveDelivery{Id: uint32(deliveryId)})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.WriteResult(w, reply)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PoolServer) QuantityChangeHandler(w http.ResponseWriter, r *http.Request, id cart.CartId) error {
|
func (s *PoolServer) QuantityChangeHandler(w http.ResponseWriter, r *http.Request, id cart.CartId) error {
|
||||||
changeQuantity := messages.ChangeQuantity{}
|
changeQuantity := messages.ChangeQuantity{}
|
||||||
err := json.NewDecoder(r.Body).Decode(&changeQuantity)
|
err := json.NewDecoder(r.Body).Decode(&changeQuantity)
|
||||||
@@ -282,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 {
|
||||||
@@ -325,99 +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) CreateOrUpdateCheckout(r *http.Request, id cart.CartId) (*CheckoutOrder, error) {
|
|
||||||
meta := GetCheckoutMetaFromRequest(r)
|
|
||||||
|
|
||||||
// Get current grain state (may be local or remote)
|
|
||||||
grain, err := s.Get(r.Context(), uint64(id))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if s.inventoryService != nil {
|
|
||||||
inventoryRequests := getInventoryRequests(grain.Items)
|
|
||||||
failingRequest, err := s.inventoryService.ReservationCheck(r.Context(), inventoryRequests...)
|
|
||||||
if err != nil {
|
|
||||||
logger.WarnContext(r.Context(), "inventory check failed", string(failingRequest.SKU), string(failingRequest.LocationID))
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build pure checkout payload
|
|
||||||
payload, _, err := BuildCheckoutOrderPayload(grain, meta)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if grain.OrderReference != "" {
|
|
||||||
return s.klarnaClient.UpdateOrder(r.Context(), grain.OrderReference, bytes.NewReader(payload))
|
|
||||||
} else {
|
|
||||||
return s.klarnaClient.CreateOrder(r.Context(), bytes.NewReader(payload))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PoolServer) ApplyCheckoutStarted(ctx context.Context, klarnaOrder *CheckoutOrder, id cart.CartId) (*actor.MutationResult[*cart.CartGrain], error) {
|
|
||||||
// Persist initialization state via mutation (best-effort)
|
|
||||||
return s.ApplyLocal(ctx, id, &messages.InitializeCheckout{
|
|
||||||
OrderId: klarnaOrder.ID,
|
|
||||||
Status: klarnaOrder.Status,
|
|
||||||
PaymentInProgress: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
||||||
@@ -431,51 +260,6 @@ func (s *PoolServer) ApplyCheckoutStarted(ctx context.Context, klarnaOrder *Chec
|
|||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
|
|
||||||
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 {
|
||||||
@@ -493,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 {
|
||||||
@@ -548,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() {
|
||||||
@@ -577,10 +330,6 @@ func (s *PoolServer) AddVoucherHandler(w http.ResponseWriter, r *http.Request, c
|
|||||||
v := voucher.Service{}
|
v := voucher.Service{}
|
||||||
msg, err := v.GetVoucher(data.VoucherCode)
|
msg, err := v.GetVoucher(data.VoucherCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.ApplyLocal(r.Context(), cartId, &messages.PreConditionFailed{
|
|
||||||
Operation: "AddVoucher",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
w.Write([]byte(err.Error()))
|
w.Write([]byte(err.Error()))
|
||||||
return err
|
return err
|
||||||
@@ -629,26 +378,6 @@ func (s *PoolServer) SubscriptionDetailsHandler(w http.ResponseWriter, r *http.R
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PoolServer) CheckoutHandler(fn func(order *CheckoutOrder, w http.ResponseWriter) error) func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
return CookieCartIdHandler(s.ProxyHandler(func(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
|
|
||||||
orderId := r.URL.Query().Get("order_id")
|
|
||||||
if orderId == "" {
|
|
||||||
order, err := s.CreateOrUpdateCheckout(r, cartId)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("unable to create klarna session", "error", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.ApplyCheckoutStarted(r.Context(), order, cartId)
|
|
||||||
return fn(order, w)
|
|
||||||
}
|
|
||||||
order, err := s.klarnaClient.GetOrder(r.Context(), orderId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return fn(order, w)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PoolServer) RemoveVoucherHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
|
func (s *PoolServer) RemoveVoucherHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
|
||||||
|
|
||||||
idStr := r.PathValue("voucherId")
|
idStr := r.PathValue("voucherId")
|
||||||
@@ -713,61 +442,27 @@ func (s *PoolServer) RemoveLineItemMarkingHandler(w http.ResponseWriter, r *http
|
|||||||
return s.WriteResult(w, reply)
|
return s.WriteResult(w, reply)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PoolServer) CreateCheckoutOrderHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
|
func (s *PoolServer) InternalApplyMutationHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
|
||||||
createCheckoutOrder := messages.CreateCheckoutOrder{}
|
if r.Method != http.MethodPost {
|
||||||
err := json.NewDecoder(r.Body).Decode(&createCheckoutOrder)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
reply, err := s.ApplyLocal(r.Context(), cartId, &createCheckoutOrder)
|
mutation := &messages.Mutation{}
|
||||||
|
err = proto.Unmarshal(data, mutation)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reply, err := s.ApplyLocal(r.Context(), cartId, mutation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return s.WriteResult(w, reply)
|
return s.WriteResult(w, reply)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionRequest struct {
|
|
||||||
SessionId string `json:"sessionId"`
|
|
||||||
SessionResult string `json:"sessionResult"`
|
|
||||||
SessionData string `json:"sessionData,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *PoolServer) 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 *PoolServer) GetAnywhere(ctx context.Context, cartId cart.CartId) (*cart.CartGrain, error) {
|
func (s *PoolServer) GetAnywhere(ctx context.Context, cartId cart.CartId) (*cart.CartGrain, error) {
|
||||||
id := uint64(cartId)
|
id := uint64(cartId)
|
||||||
if host, isOnOtherHost := s.OwnerHost(id); isOnOtherHost {
|
if host, isOnOtherHost := s.OwnerHost(id); isOnOtherHost {
|
||||||
@@ -789,164 +484,6 @@ func (s *PoolServer) ApplyAnywhere(ctx context.Context, cartId cart.CartId, msgs
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PoolServer) 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(checkout.PaymentCaptureRequest{
|
|
||||||
Amount: checkout.Amount{
|
|
||||||
Currency: meta.Currency,
|
|
||||||
Value: grain.TotalPrice.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 *PoolServer) 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(checkout.PaymentDetailsRequest{
|
|
||||||
Details: checkout.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 *PoolServer) Serve(mux *http.ServeMux) {
|
func (s *PoolServer) Serve(mux *http.ServeMux) {
|
||||||
|
|
||||||
// mux.HandleFunc("OPTIONS /cart", func(w http.ResponseWriter, r *http.Request) {
|
// mux.HandleFunc("OPTIONS /cart", func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -970,9 +507,6 @@ func (s *PoolServer) Serve(mux *http.ServeMux) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFunc("/adyen_hook", s.AdyenHookHandler)
|
|
||||||
handleFunc("/adyen-return", s.AdyenReturnHandler)
|
|
||||||
|
|
||||||
handleFunc("GET /cart", CookieCartIdHandler(s.ProxyHandler(s.GetCartHandler)))
|
handleFunc("GET /cart", CookieCartIdHandler(s.ProxyHandler(s.GetCartHandler)))
|
||||||
handleFunc("GET /cart/add/{sku}", CookieCartIdHandler(s.ProxyHandler(s.AddSkuToCartHandler)))
|
handleFunc("GET /cart/add/{sku}", CookieCartIdHandler(s.ProxyHandler(s.AddSkuToCartHandler)))
|
||||||
handleFunc("POST /cart/add", CookieCartIdHandler(s.ProxyHandler(s.AddMultipleItemHandler)))
|
handleFunc("POST /cart/add", CookieCartIdHandler(s.ProxyHandler(s.AddMultipleItemHandler)))
|
||||||
@@ -981,38 +515,25 @@ func (s *PoolServer) Serve(mux *http.ServeMux) {
|
|||||||
handleFunc("DELETE /cart/{itemId}", CookieCartIdHandler(s.ProxyHandler(s.DeleteItemHandler)))
|
handleFunc("DELETE /cart/{itemId}", CookieCartIdHandler(s.ProxyHandler(s.DeleteItemHandler)))
|
||||||
handleFunc("PUT /cart", CookieCartIdHandler(s.ProxyHandler(s.QuantityChangeHandler)))
|
handleFunc("PUT /cart", CookieCartIdHandler(s.ProxyHandler(s.QuantityChangeHandler)))
|
||||||
handleFunc("DELETE /cart", CookieCartIdHandler(s.ProxyHandler(s.RemoveCartCookie)))
|
handleFunc("DELETE /cart", CookieCartIdHandler(s.ProxyHandler(s.RemoveCartCookie)))
|
||||||
handleFunc("POST /cart/delivery", CookieCartIdHandler(s.ProxyHandler(s.SetDeliveryHandler)))
|
|
||||||
handleFunc("DELETE /cart/delivery/{deliveryId}", CookieCartIdHandler(s.ProxyHandler(s.RemoveDeliveryHandler)))
|
|
||||||
handleFunc("PUT /cart/delivery/{deliveryId}/pickupPoint", CookieCartIdHandler(s.ProxyHandler(s.SetPickupPointHandler)))
|
|
||||||
handleFunc("PUT /cart/voucher", CookieCartIdHandler(s.ProxyHandler(s.AddVoucherHandler)))
|
handleFunc("PUT /cart/voucher", CookieCartIdHandler(s.ProxyHandler(s.AddVoucherHandler)))
|
||||||
handleFunc("PUT /cart/subscription-details", CookieCartIdHandler(s.ProxyHandler(s.SubscriptionDetailsHandler)))
|
handleFunc("PUT /cart/subscription-details", CookieCartIdHandler(s.ProxyHandler(s.SubscriptionDetailsHandler)))
|
||||||
handleFunc("DELETE /cart/voucher/{voucherId}", CookieCartIdHandler(s.ProxyHandler(s.RemoveVoucherHandler)))
|
handleFunc("DELETE /cart/voucher/{voucherId}", CookieCartIdHandler(s.ProxyHandler(s.RemoveVoucherHandler)))
|
||||||
handleFunc("PUT /cart/user", CookieCartIdHandler(s.ProxyHandler(s.SetUserIdHandler)))
|
handleFunc("PUT /cart/user", CookieCartIdHandler(s.ProxyHandler(s.SetUserIdHandler)))
|
||||||
handleFunc("GET /cart/adyen-session", CookieCartIdHandler(s.ProxyHandler(s.AdyenSessionHandler)))
|
|
||||||
handleFunc("PUT /cart/item/{itemId}/marking", CookieCartIdHandler(s.ProxyHandler(s.LineItemMarkingHandler)))
|
handleFunc("PUT /cart/item/{itemId}/marking", CookieCartIdHandler(s.ProxyHandler(s.LineItemMarkingHandler)))
|
||||||
handleFunc("DELETE /cart/item/{itemId}/marking", CookieCartIdHandler(s.ProxyHandler(s.RemoveLineItemMarkingHandler)))
|
handleFunc("DELETE /cart/item/{itemId}/marking", CookieCartIdHandler(s.ProxyHandler(s.RemoveLineItemMarkingHandler)))
|
||||||
|
|
||||||
handleFunc("POST /cart/checkout-order", CookieCartIdHandler(s.ProxyHandler(s.CreateCheckoutOrderHandler)))
|
|
||||||
|
|
||||||
//mux.HandleFunc("GET /cart/checkout", CookieCartIdHandler(s.ProxyHandler(s.HandleCheckout)))
|
//mux.HandleFunc("GET /cart/checkout", CookieCartIdHandler(s.ProxyHandler(s.HandleCheckout)))
|
||||||
//mux.HandleFunc("GET /cart/confirmation/{orderId}", CookieCartIdHandler(s.ProxyHandler(s.HandleConfirmation)))
|
//mux.HandleFunc("GET /cart/confirmation/{orderId}", CookieCartIdHandler(s.ProxyHandler(s.HandleConfirmation)))
|
||||||
|
|
||||||
handleFunc("GET /cart/byid/{id}", CartIdHandler(s.ProxyHandler(s.GetCartHandler)))
|
handleFunc("GET /cart/byid/{id}", CartIdHandler(s.ProxyHandler(s.GetCartHandler)))
|
||||||
handleFunc("GET /cart/byid/{id}/add/{sku}", CartIdHandler(s.ProxyHandler(s.AddSkuToCartHandler)))
|
|
||||||
handleFunc("POST /cart/byid/{id}", CartIdHandler(s.ProxyHandler(s.AddSkuRequestHandler)))
|
handleFunc("POST /cart/byid/{id}", CartIdHandler(s.ProxyHandler(s.AddSkuRequestHandler)))
|
||||||
handleFunc("DELETE /cart/byid/{id}/{itemId}", CartIdHandler(s.ProxyHandler(s.DeleteItemHandler)))
|
handleFunc("DELETE /cart/byid/{id}/{itemId}", CartIdHandler(s.ProxyHandler(s.DeleteItemHandler)))
|
||||||
handleFunc("PUT /cart/byid/{id}", CartIdHandler(s.ProxyHandler(s.QuantityChangeHandler)))
|
handleFunc("PUT /cart/byid/{id}", CartIdHandler(s.ProxyHandler(s.QuantityChangeHandler)))
|
||||||
handleFunc("POST /cart/byid/{id}/delivery", CartIdHandler(s.ProxyHandler(s.SetDeliveryHandler)))
|
|
||||||
handleFunc("DELETE /cart/byid/{id}/delivery/{deliveryId}", CartIdHandler(s.ProxyHandler(s.RemoveDeliveryHandler)))
|
|
||||||
handleFunc("PUT /cart/byid/{id}/delivery/{deliveryId}/pickupPoint", CartIdHandler(s.ProxyHandler(s.SetPickupPointHandler)))
|
|
||||||
handleFunc("PUT /cart/byid/{id}/voucher", CookieCartIdHandler(s.ProxyHandler(s.AddVoucherHandler)))
|
handleFunc("PUT /cart/byid/{id}/voucher", CookieCartIdHandler(s.ProxyHandler(s.AddVoucherHandler)))
|
||||||
handleFunc("DELETE /cart/byid/{id}/voucher/{voucherId}", CookieCartIdHandler(s.ProxyHandler(s.RemoveVoucherHandler)))
|
handleFunc("DELETE /cart/byid/{id}/voucher/{voucherId}", CookieCartIdHandler(s.ProxyHandler(s.RemoveVoucherHandler)))
|
||||||
handleFunc("PUT /cart/byid/{id}/user", CartIdHandler(s.ProxyHandler(s.SetUserIdHandler)))
|
handleFunc("PUT /cart/byid/{id}/user", CartIdHandler(s.ProxyHandler(s.SetUserIdHandler)))
|
||||||
handleFunc("PUT /cart/byid/{id}/item/{itemId}/marking", CartIdHandler(s.ProxyHandler(s.LineItemMarkingHandler)))
|
handleFunc("PUT /cart/byid/{id}/item/{itemId}/marking", CartIdHandler(s.ProxyHandler(s.LineItemMarkingHandler)))
|
||||||
handleFunc("DELETE /cart/byid/{id}/item/{itemId}/marking", CartIdHandler(s.ProxyHandler(s.RemoveLineItemMarkingHandler)))
|
handleFunc("DELETE /cart/byid/{id}/item/{itemId}/marking", CartIdHandler(s.ProxyHandler(s.RemoveLineItemMarkingHandler)))
|
||||||
|
|
||||||
handleFunc("POST /cart/byid/{id}/checkout-order", CartIdHandler(s.ProxyHandler(s.CreateCheckoutOrderHandler)))
|
|
||||||
//mux.HandleFunc("GET /cart/byid/{id}/checkout", CartIdHandler(s.ProxyHandler(s.HandleCheckout)))
|
|
||||||
//mux.HandleFunc("GET /cart/byid/{id}/confirmation", CartIdHandler(s.ProxyHandler(s.HandleConfirmation)))
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.k6n.net/go-cart-actor/pkg/cart"
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||||
|
|
||||||
"github.com/matst80/slask-finder/pkg/index"
|
"github.com/matst80/slask-finder/pkg/index"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
228
cmd/checkout/adyen-handlers.go
Normal file
228
cmd/checkout/adyen-handlers.go
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"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 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 {
|
||||||
|
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, err := getCheckoutIdFromNotificationItem(item)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 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)
|
||||||
|
}
|
||||||
65
cmd/checkout/cart-client.go
Normal file
65
cmd/checkout/cart-client.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 (s *CartClient) getCartGrain(ctx context.Context, cartId cart.CartId) (*cart.CartGrain, error) {
|
||||||
|
// Call cart service to get grain
|
||||||
|
url := fmt.Sprintf("%s/cart/byid/%d", s.baseUrl, cartId.String())
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp, err := s.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("failed to get cart: %s", resp.Status)
|
||||||
|
}
|
||||||
|
var grain cart.CartGrain
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&grain)
|
||||||
|
return &grain, err
|
||||||
|
}
|
||||||
@@ -6,7 +6,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.k6n.net/go-cart-actor/pkg/cart"
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
"github.com/adyen/adyen-go-api-library/v21/src/checkout"
|
"git.k6n.net/go-cart-actor/pkg/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/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ type CheckoutMeta struct {
|
|||||||
//
|
//
|
||||||
// If you later need to support different tax rates per line, you can extend
|
// If you later need to support different tax rates per line, you can extend
|
||||||
// CartItem / Delivery to expose that data and propagate it here.
|
// CartItem / Delivery to expose that data and propagate it here.
|
||||||
func BuildCheckoutOrderPayload(grain *cart.CartGrain, meta *CheckoutMeta) ([]byte, *CheckoutOrder, error) {
|
func BuildCheckoutOrderPayload(grain *checkout.CheckoutGrain, meta *CheckoutMeta) ([]byte, *CheckoutOrder, error) {
|
||||||
if grain == nil {
|
if grain == nil {
|
||||||
return nil, nil, fmt.Errorf("nil grain")
|
return nil, nil, fmt.Errorf("nil grain")
|
||||||
}
|
}
|
||||||
@@ -59,10 +60,10 @@ func BuildCheckoutOrderPayload(grain *cart.CartGrain, meta *CheckoutMeta) ([]byt
|
|||||||
country = "SE" // sensible default; adjust if multi-country support changes
|
country = "SE" // sensible default; adjust if multi-country support changes
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := make([]*Line, 0, len(grain.Items)+len(grain.Deliveries))
|
lines := make([]*Line, 0, len(grain.CartState.Items)+len(grain.Deliveries))
|
||||||
|
|
||||||
// Item lines
|
// Item lines
|
||||||
for _, it := range grain.Items {
|
for _, it := range grain.CartState.Items {
|
||||||
if it == nil {
|
if it == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -80,11 +81,15 @@ func BuildCheckoutOrderPayload(grain *cart.CartGrain, meta *CheckoutMeta) ([]byt
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
total := cart.NewPrice()
|
||||||
|
total.Add(*grain.CartState.TotalPrice)
|
||||||
|
|
||||||
// Delivery lines
|
// Delivery lines
|
||||||
for _, d := range grain.Deliveries {
|
for _, d := range grain.Deliveries {
|
||||||
if d == nil || d.Price.IncVat <= 0 {
|
if d == nil || d.Price.IncVat <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
//total.Add(d.Price)
|
||||||
lines = append(lines, &Line{
|
lines = append(lines, &Line{
|
||||||
Type: "shipping_fee",
|
Type: "shipping_fee",
|
||||||
Reference: d.Provider,
|
Reference: d.Provider,
|
||||||
@@ -102,8 +107,8 @@ func BuildCheckoutOrderPayload(grain *cart.CartGrain, meta *CheckoutMeta) ([]byt
|
|||||||
PurchaseCountry: country,
|
PurchaseCountry: country,
|
||||||
PurchaseCurrency: currency,
|
PurchaseCurrency: currency,
|
||||||
Locale: locale,
|
Locale: locale,
|
||||||
OrderAmount: int(grain.TotalPrice.IncVat),
|
OrderAmount: int(total.IncVat),
|
||||||
OrderTaxAmount: int(grain.TotalPrice.TotalVat()),
|
OrderTaxAmount: int(total.TotalVat()),
|
||||||
OrderLines: lines,
|
OrderLines: lines,
|
||||||
MerchantReference1: grain.Id.String(),
|
MerchantReference1: grain.Id.String(),
|
||||||
MerchantURLS: &CheckoutMerchantURLS{
|
MerchantURLS: &CheckoutMerchantURLS{
|
||||||
@@ -136,7 +141,7 @@ func GetCheckoutMetaFromRequest(r *http.Request) *CheckoutMeta {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildAdyenCheckoutSession(grain *cart.CartGrain, meta *CheckoutMeta) (*checkout.CreateCheckoutSessionRequest, error) {
|
func BuildAdyenCheckoutSession(grain *checkout.CheckoutGrain, meta *CheckoutMeta) (*adyenCheckout.CreateCheckoutSessionRequest, error) {
|
||||||
if grain == nil {
|
if grain == nil {
|
||||||
return nil, fmt.Errorf("nil grain")
|
return nil, fmt.Errorf("nil grain")
|
||||||
}
|
}
|
||||||
@@ -153,14 +158,14 @@ func BuildAdyenCheckoutSession(grain *cart.CartGrain, meta *CheckoutMeta) (*chec
|
|||||||
country = "SE"
|
country = "SE"
|
||||||
}
|
}
|
||||||
|
|
||||||
lineItems := make([]checkout.LineItem, 0, len(grain.Items)+len(grain.Deliveries))
|
lineItems := make([]adyenCheckout.LineItem, 0, len(grain.CartState.Items)+len(grain.Deliveries))
|
||||||
|
|
||||||
// Item lines
|
// Item lines
|
||||||
for _, it := range grain.Items {
|
for _, it := range grain.CartState.Items {
|
||||||
if it == nil {
|
if it == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
lineItems = append(lineItems, checkout.LineItem{
|
lineItems = append(lineItems, adyenCheckout.LineItem{
|
||||||
Quantity: common.PtrInt64(int64(it.Quantity)),
|
Quantity: common.PtrInt64(int64(it.Quantity)),
|
||||||
AmountIncludingTax: common.PtrInt64(it.TotalPrice.IncVat),
|
AmountIncludingTax: common.PtrInt64(it.TotalPrice.IncVat),
|
||||||
Description: common.PtrString(it.Meta.Name),
|
Description: common.PtrString(it.Meta.Name),
|
||||||
@@ -169,13 +174,15 @@ func BuildAdyenCheckoutSession(grain *cart.CartGrain, meta *CheckoutMeta) (*chec
|
|||||||
TaxPercentage: common.PtrInt64(int64(it.Tax)),
|
TaxPercentage: common.PtrInt64(int64(it.Tax)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
total := cart.NewPrice()
|
||||||
|
total.Add(*grain.CartState.TotalPrice)
|
||||||
|
|
||||||
// Delivery lines
|
// Delivery lines
|
||||||
for _, d := range grain.Deliveries {
|
for _, d := range grain.Deliveries {
|
||||||
if d == nil || d.Price.IncVat <= 0 {
|
if d == nil || d.Price.IncVat <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
lineItems = append(lineItems, checkout.LineItem{
|
lineItems = append(lineItems, adyenCheckout.LineItem{
|
||||||
Quantity: common.PtrInt64(1),
|
Quantity: common.PtrInt64(1),
|
||||||
AmountIncludingTax: common.PtrInt64(d.Price.IncVat),
|
AmountIncludingTax: common.PtrInt64(d.Price.IncVat),
|
||||||
Description: common.PtrString("Delivery"),
|
Description: common.PtrString("Delivery"),
|
||||||
@@ -184,10 +191,10 @@ func BuildAdyenCheckoutSession(grain *cart.CartGrain, meta *CheckoutMeta) (*chec
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return &checkout.CreateCheckoutSessionRequest{
|
return &adyenCheckout.CreateCheckoutSessionRequest{
|
||||||
Reference: grain.Id.String(),
|
Reference: grain.Id.String(),
|
||||||
Amount: checkout.Amount{
|
Amount: adyenCheckout.Amount{
|
||||||
Value: grain.TotalPrice.IncVat,
|
Value: total.IncVat,
|
||||||
Currency: currency,
|
Currency: currency,
|
||||||
},
|
},
|
||||||
CountryCode: common.PtrString(country),
|
CountryCode: common.PtrString(country),
|
||||||
65
cmd/checkout/k8s-host-discovery.go
Normal file
65
cmd/checkout/k8s-host-discovery.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/checkout"
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/discovery"
|
||||||
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetDiscovery() discovery.Discovery {
|
||||||
|
if podIp == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
config, kerr := rest.InClusterConfig()
|
||||||
|
|
||||||
|
if kerr != nil {
|
||||||
|
log.Fatalf("Error creating kubernetes client: %v\n", kerr)
|
||||||
|
}
|
||||||
|
client, err := kubernetes.NewForConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error creating client: %v\n", err)
|
||||||
|
}
|
||||||
|
timeout := int64(30)
|
||||||
|
return discovery.NewK8sDiscovery(client, v1.ListOptions{
|
||||||
|
LabelSelector: "actor-pool=checkout",
|
||||||
|
TimeoutSeconds: &timeout,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func UseDiscovery(pool actor.GrainPool[*checkout.CheckoutGrain]) {
|
||||||
|
|
||||||
|
go func(hw discovery.Discovery) {
|
||||||
|
if hw == nil {
|
||||||
|
log.Print("No discovery service available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ch, err := hw.Watch()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Discovery error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for evt := range ch {
|
||||||
|
if evt.Host == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch evt.IsReady {
|
||||||
|
case false:
|
||||||
|
if pool.IsKnown(evt.Host) {
|
||||||
|
log.Printf("Host %s is not ready, removing", evt.Host)
|
||||||
|
pool.RemoveHost(evt.Host)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if !pool.IsKnown(evt.Host) {
|
||||||
|
log.Printf("Discovered host %s", evt.Host)
|
||||||
|
pool.AddRemoteHost(evt.Host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(GetDiscovery())
|
||||||
|
}
|
||||||
242
cmd/checkout/klarna-handlers.go
Normal file
242
cmd/checkout/klarna-handlers.go
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) KlarnaHtmlCheckoutHandler(w http.ResponseWriter, r *http.Request, checkoutId checkout.CheckoutId) error {
|
||||||
|
|
||||||
|
orderId := r.URL.Query().Get("order_id")
|
||||||
|
var order *CheckoutOrder
|
||||||
|
var err error
|
||||||
|
if orderId == "" {
|
||||||
|
order, err = s.CreateOrUpdateCheckout(r, checkoutId)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("unable to create klarna session", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// s.ApplyKlarnaPaymentStarted(r.Context(), order, checkoutId)
|
||||||
|
|
||||||
|
}
|
||||||
|
order, err = s.klarnaClient.GetOrder(r.Context(), orderId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) KlarnaConfirmationHandler(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 {
|
||||||
|
s.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)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
196
cmd/checkout/main.go
Normal file
196
cmd/checkout/main.go
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/checkout"
|
||||||
|
"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/common"
|
||||||
|
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
grainSpawns = promauto.NewCounter(prometheus.CounterOpts{
|
||||||
|
Name: "checkout_grain_spawned_total",
|
||||||
|
Help: "The total number of spawned checkout grains",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
os.Mkdir("data", 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
pool *actor.SimpleGrainPool[checkout.CheckoutGrain]
|
||||||
|
server *CheckoutPoolServer
|
||||||
|
klarnaClient *KlarnaClient
|
||||||
|
cartClient *CartClient // For internal communication to cart
|
||||||
|
}
|
||||||
|
|
||||||
|
var podIp = os.Getenv("POD_IP")
|
||||||
|
var name = os.Getenv("POD_NAME")
|
||||||
|
var amqpUrl = os.Getenv("AMQP_URL")
|
||||||
|
var redisAddress = os.Getenv("REDIS_ADDRESS")
|
||||||
|
var redisPassword = os.Getenv("REDIS_PASSWORD")
|
||||||
|
var cartInternalUrl = os.Getenv("CART_INTERNAL_URL") // e.g., http://cart-service:8081
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
controlPlaneConfig := actor.DefaultServerConfig()
|
||||||
|
|
||||||
|
reg := checkout.NewCheckoutMutationRegistry(checkout.NewCheckoutMutationContext())
|
||||||
|
reg.RegisterProcessor(
|
||||||
|
actor.NewMutationProcessor(func(ctx context.Context, g *checkout.CheckoutGrain) error {
|
||||||
|
g.Version++
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
rdb := redis.NewClient(&redis.Options{
|
||||||
|
Addr: redisAddress,
|
||||||
|
Password: redisPassword,
|
||||||
|
DB: 0,
|
||||||
|
})
|
||||||
|
inventoryService, err := inventory.NewRedisInventoryService(rdb)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error creating inventory service: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
diskStorage := actor.NewDiskStorage[checkout.CheckoutGrain]("data", reg)
|
||||||
|
|
||||||
|
poolConfig := actor.GrainPoolConfig[checkout.CheckoutGrain]{
|
||||||
|
MutationRegistry: reg,
|
||||||
|
Storage: diskStorage,
|
||||||
|
Spawn: func(ctx context.Context, id uint64) (actor.Grain[checkout.CheckoutGrain], error) {
|
||||||
|
_, span := tracer.Start(ctx, fmt.Sprintf("Spawn checkout id %d", id))
|
||||||
|
defer span.End()
|
||||||
|
grainSpawns.Inc()
|
||||||
|
|
||||||
|
ret := checkout.NewCheckoutGrain(id, 0, 0, time.Now(), nil) // version to be set later
|
||||||
|
return ret, nil
|
||||||
|
},
|
||||||
|
Destroy: func(grain actor.Grain[checkout.CheckoutGrain]) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
SpawnHost: func(host string) (actor.Host, error) {
|
||||||
|
return proxy.NewRemoteHost[checkout.CheckoutGrain](host)
|
||||||
|
},
|
||||||
|
TTL: 1 * time.Hour, // Longer TTL for checkout
|
||||||
|
PoolSize: 65535,
|
||||||
|
Hostname: podIp,
|
||||||
|
}
|
||||||
|
|
||||||
|
pool, err := actor.NewSimpleGrainPool(poolConfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error creating checkout pool: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
adyenClient := adyen.NewClient(&common.Config{
|
||||||
|
ApiKey: os.Getenv("ADYEN_API_KEY"),
|
||||||
|
Environment: common.TestEnv,
|
||||||
|
})
|
||||||
|
|
||||||
|
klarnaClient := NewKlarnaClient(KlarnaPlaygroundUrl, os.Getenv("KLARNA_API_USERNAME"), os.Getenv("KLARNA_API_PASSWORD"))
|
||||||
|
|
||||||
|
cartClient := NewCartClient(cartInternalUrl)
|
||||||
|
|
||||||
|
syncedServer := NewCheckoutPoolServer(pool, fmt.Sprintf("%s, %s", name, podIp), klarnaClient, cartClient, adyenClient)
|
||||||
|
syncedServer.inventoryService = inventoryService
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
debugMux := http.NewServeMux()
|
||||||
|
|
||||||
|
if amqpUrl == "" {
|
||||||
|
log.Fatalf("no connection to amqp defined")
|
||||||
|
}
|
||||||
|
|
||||||
|
grpcSrv, err := actor.NewControlServer[*checkout.CheckoutGrain](controlPlaneConfig, pool)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error starting control plane gRPC server: %v\n", err)
|
||||||
|
}
|
||||||
|
defer grpcSrv.GracefulStop()
|
||||||
|
|
||||||
|
UseDiscovery(pool)
|
||||||
|
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
otelShutdown, err := setupOTelSDK(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Unable to start otel %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
syncedServer.Serve(mux)
|
||||||
|
|
||||||
|
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
grainCount, capacity := pool.LocalUsage()
|
||||||
|
if grainCount >= capacity {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write([]byte("grain pool at capacity"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !pool.IsHealthy() {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write([]byte("control plane not healthy"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/livez", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("1.0.0"))
|
||||||
|
})
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: ":8080",
|
||||||
|
BaseContext: func(net.Listener) context.Context { return ctx },
|
||||||
|
ReadTimeout: 10 * time.Second,
|
||||||
|
WriteTimeout: 20 * time.Second,
|
||||||
|
Handler: otelhttp.NewHandler(mux, "/"),
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
fmt.Println("Shutting down due to signal")
|
||||||
|
otelShutdown(context.Background())
|
||||||
|
diskStorage.Close()
|
||||||
|
pool.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
srvErr := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
srvErr <- srv.ListenAndServe()
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Print("Checkout server started at port 8080")
|
||||||
|
|
||||||
|
go http.ListenAndServe(":8081", debugMux)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err = <-srvErr:
|
||||||
|
log.Fatalf("Unable to start server: %v", err)
|
||||||
|
case <-ctx.Done():
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
117
cmd/checkout/otel.go
Normal file
117
cmd/checkout/otel.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||||
|
"go.opentelemetry.io/otel/log/global"
|
||||||
|
"go.opentelemetry.io/otel/propagation"
|
||||||
|
"go.opentelemetry.io/otel/sdk/log"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupOTelSDK bootstraps the OpenTelemetry pipeline.
|
||||||
|
// If it does not return an error, make sure to call shutdown for proper cleanup.
|
||||||
|
func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) {
|
||||||
|
var shutdownFuncs []func(context.Context) error
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// shutdown calls cleanup functions registered via shutdownFuncs.
|
||||||
|
// The errors from the calls are joined.
|
||||||
|
// Each registered cleanup will be invoked once.
|
||||||
|
shutdown := func(ctx context.Context) error {
|
||||||
|
var err error
|
||||||
|
for _, fn := range shutdownFuncs {
|
||||||
|
err = errors.Join(err, fn(ctx))
|
||||||
|
}
|
||||||
|
shutdownFuncs = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleErr calls shutdown for cleanup and makes sure that all errors are returned.
|
||||||
|
handleErr := func(inErr error) {
|
||||||
|
err = errors.Join(inErr, shutdown(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up propagator.
|
||||||
|
prop := newPropagator()
|
||||||
|
otel.SetTextMapPropagator(prop)
|
||||||
|
|
||||||
|
// Set up trace provider.
|
||||||
|
tracerProvider, err := newTracerProvider()
|
||||||
|
if err != nil {
|
||||||
|
handleErr(err)
|
||||||
|
return shutdown, err
|
||||||
|
}
|
||||||
|
shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
|
||||||
|
otel.SetTracerProvider(tracerProvider)
|
||||||
|
|
||||||
|
// Set up meter provider.
|
||||||
|
meterProvider, err := newMeterProvider()
|
||||||
|
if err != nil {
|
||||||
|
handleErr(err)
|
||||||
|
return shutdown, err
|
||||||
|
}
|
||||||
|
shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown)
|
||||||
|
otel.SetMeterProvider(meterProvider)
|
||||||
|
|
||||||
|
// Set up logger provider.
|
||||||
|
loggerProvider, err := newLoggerProvider()
|
||||||
|
if err != nil {
|
||||||
|
handleErr(err)
|
||||||
|
return shutdown, err
|
||||||
|
}
|
||||||
|
shutdownFuncs = append(shutdownFuncs, loggerProvider.Shutdown)
|
||||||
|
global.SetLoggerProvider(loggerProvider)
|
||||||
|
|
||||||
|
return shutdown, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPropagator() propagation.TextMapPropagator {
|
||||||
|
return propagation.NewCompositeTextMapPropagator(
|
||||||
|
propagation.TraceContext{},
|
||||||
|
propagation.Baggage{},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTracerProvider() (*trace.TracerProvider, error) {
|
||||||
|
traceExporter, err := otlptracegrpc.New(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tracerProvider := trace.NewTracerProvider(
|
||||||
|
trace.WithBatcher(traceExporter,
|
||||||
|
// Default is 5s. Set to 1s for demonstrative purposes.
|
||||||
|
trace.WithBatchTimeout(time.Second)),
|
||||||
|
)
|
||||||
|
return tracerProvider, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMeterProvider() (*metric.MeterProvider, error) {
|
||||||
|
exporter, err := otlpmetricgrpc.New(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := metric.NewMeterProvider(metric.WithReader(metric.NewPeriodicReader(exporter)))
|
||||||
|
return provider, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLoggerProvider() (*log.LoggerProvider, error) {
|
||||||
|
logExporter, err := otlploggrpc.New(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
loggerProvider := log.NewLoggerProvider(
|
||||||
|
log.WithProcessor(log.NewBatchProcessor(logExporter)),
|
||||||
|
)
|
||||||
|
return loggerProvider, nil
|
||||||
|
}
|
||||||
237
cmd/checkout/pool-server.go
Normal file
237
cmd/checkout/pool-server.go
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/checkout"
|
||||||
|
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||||
|
|
||||||
|
adyen "github.com/adyen/adyen-go-api-library/v21/src/adyen"
|
||||||
|
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
||||||
|
amqp "github.com/rabbitmq/amqp091-go"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
"go.opentelemetry.io/contrib/bridges/otelslog"
|
||||||
|
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
grainMutations = promauto.NewCounter(prometheus.CounterOpts{
|
||||||
|
Name: "checkout_grain_mutations_total",
|
||||||
|
Help: "The total number of mutations",
|
||||||
|
})
|
||||||
|
grainLookups = promauto.NewCounter(prometheus.CounterOpts{
|
||||||
|
Name: "checkout_grain_lookups_total",
|
||||||
|
Help: "The total number of lookups",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
type CheckoutPoolServer struct {
|
||||||
|
actor.GrainPool[*checkout.CheckoutGrain]
|
||||||
|
pod_name string
|
||||||
|
klarnaClient *KlarnaClient
|
||||||
|
adyenClient *adyen.APIClient
|
||||||
|
cartClient *CartClient
|
||||||
|
inventoryService *inventory.RedisInventoryService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCheckoutPoolServer(pool actor.GrainPool[*checkout.CheckoutGrain], pod_name string, klarnaClient *KlarnaClient, cartClient *CartClient, adyenClient *adyen.APIClient) *CheckoutPoolServer {
|
||||||
|
srv := &CheckoutPoolServer{
|
||||||
|
GrainPool: pool,
|
||||||
|
pod_name: pod_name,
|
||||||
|
klarnaClient: klarnaClient,
|
||||||
|
cartClient: cartClient,
|
||||||
|
adyenClient: adyenClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
return srv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) ApplyLocal(ctx context.Context, id checkout.CheckoutId, mutation ...proto.Message) (*actor.MutationResult[*checkout.CheckoutGrain], error) {
|
||||||
|
return s.Apply(ctx, uint64(id), mutation...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) GetCheckoutHandler(w http.ResponseWriter, r *http.Request, id checkout.CheckoutId) error {
|
||||||
|
grain, err := s.Get(r.Context(), uint64(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.WriteResult(w, grain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) WriteResult(w http.ResponseWriter, result any) error {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("X-Pod-Name", s.pod_name)
|
||||||
|
if result == nil {
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
err := enc.Encode(result)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) CreateOrUpdateCheckout(r *http.Request, id checkout.CheckoutId) (*CheckoutOrder, error) {
|
||||||
|
// Get cart state from cart service
|
||||||
|
cartGrain, err := s.Get(r.Context(), uint64(id))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
meta := GetCheckoutMetaFromRequest(r)
|
||||||
|
|
||||||
|
payload, _, err := BuildCheckoutOrderPayload(cartGrain, meta)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
grain, err := s.Get(r.Context(), uint64(id))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if grain.OrderId != nil {
|
||||||
|
return s.klarnaClient.UpdateOrder(r.Context(), *grain.OrderId, bytes.NewReader(payload))
|
||||||
|
} else {
|
||||||
|
return s.klarnaClient.CreateOrder(r.Context(), bytes.NewReader(payload))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) ApplyKlarnaPaymentStarted(ctx context.Context, klarnaOrder *CheckoutOrder, id checkout.CheckoutId) (*actor.MutationResult[*checkout.CheckoutGrain], error) {
|
||||||
|
method := "checkout"
|
||||||
|
return s.ApplyLocal(ctx, id, &messages.PaymentStarted{
|
||||||
|
PaymentId: klarnaOrder.ID,
|
||||||
|
Amount: int64(klarnaOrder.OrderAmount),
|
||||||
|
Currency: klarnaOrder.PurchaseCurrency,
|
||||||
|
Provider: "klarna",
|
||||||
|
Method: &method,
|
||||||
|
StartedAt: timestamppb.New(time.Now()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (s *CheckoutPoolServer) CheckoutHandler(fn func(order *CheckoutOrder, w http.ResponseWriter) error) func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// return CheckoutIdHandler(s.ProxyHandler(func(w http.ResponseWriter, r *http.Request, checkoutId checkout.CheckoutId) error {
|
||||||
|
// orderId := r.URL.Query().Get("order_id")
|
||||||
|
// if orderId == "" {
|
||||||
|
// order, err := s.CreateOrUpdateCheckout(r, checkoutId)
|
||||||
|
// if err != nil {
|
||||||
|
// logger.Error("unable to create klarna session", "error", err)
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// s.ApplyKlarnaPaymentStarted(r.Context(), order, checkoutId)
|
||||||
|
// return fn(order, w)
|
||||||
|
// }
|
||||||
|
// order, err := s.klarnaClient.GetOrder(r.Context(), orderId)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// return fn(order, w)
|
||||||
|
// }))
|
||||||
|
// }
|
||||||
|
|
||||||
|
var (
|
||||||
|
tracer = otel.Tracer(name)
|
||||||
|
hmacKey = os.Getenv("ADYEN_HMAC")
|
||||||
|
meter = otel.Meter(name)
|
||||||
|
logger = otelslog.NewLogger(name)
|
||||||
|
proxyCalls metric.Int64Counter
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var err error
|
||||||
|
proxyCalls, err = meter.Int64Counter("proxy.calls",
|
||||||
|
metric.WithDescription("Number of proxy calls"),
|
||||||
|
metric.WithUnit("{calls}"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) Serve(mux *http.ServeMux) {
|
||||||
|
handleFunc := func(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) {
|
||||||
|
attr := attribute.String("http.route", pattern)
|
||||||
|
mux.HandleFunc(pattern, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
span := trace.SpanFromContext(r.Context())
|
||||||
|
span.SetName(pattern)
|
||||||
|
span.SetAttributes(attr)
|
||||||
|
|
||||||
|
labeler, _ := otelhttp.LabelerFromContext(r.Context())
|
||||||
|
labeler.Add(attr)
|
||||||
|
|
||||||
|
handlerFunc(w, r)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
handleFunc("/payment/adyen/session", CheckoutIdHandler(s.AdyenSessionHandler))
|
||||||
|
handleFunc("/payment/adyen/push", s.AdyenHookHandler)
|
||||||
|
handleFunc("/payment/adyen/return", s.AdyenReturnHandler)
|
||||||
|
//handleFunc("/payment/adyen/cancel", s.AdyenCancelHandler)
|
||||||
|
|
||||||
|
handleFunc("/payment/klarna/validate", s.KlarnaValidationHandler)
|
||||||
|
handleFunc("/payment/klarna/notification", s.KlarnaNotificationHandler)
|
||||||
|
|
||||||
|
conn, err := amqp.Dial(amqpUrl)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to connect to RabbitMQ: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
orderHandler := NewAmqpOrderHandler(conn)
|
||||||
|
orderHandler.DefineQueue()
|
||||||
|
|
||||||
|
handleFunc("GET /checkout", CheckoutIdHandler(s.ProxyHandler(s.KlarnaHtmlCheckoutHandler)))
|
||||||
|
|
||||||
|
handleFunc("GET /confirmation/{order_id}", s.KlarnaConfirmationHandler)
|
||||||
|
|
||||||
|
// handleFunc("GET /checkout", s.CheckoutHandler(func(order *CheckoutOrder, w http.ResponseWriter) error {
|
||||||
|
// w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
// w.Header().Set("Permissions-Policy", "payment=(self \"https://js.stripe.com\" \"https://m.stripe.network\" \"https://js.playground.kustom.co\")")
|
||||||
|
// w.WriteHeader(http.StatusOK)
|
||||||
|
// _, err := fmt.Fprintf(w, tpl, order.HTMLSnippet)
|
||||||
|
// return err
|
||||||
|
// }))
|
||||||
|
|
||||||
|
handleFunc("GET /confirmation/{order_id}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
orderId := r.PathValue("order_id")
|
||||||
|
order, err := s.klarnaClient.GetOrder(r.Context(), orderId)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply ConfirmationViewed to checkout
|
||||||
|
checkoutId := checkout.CheckoutId(cart.MustNewCartId()) // Need to resolve from order
|
||||||
|
s.Apply(r.Context(), uint64(checkoutId), &messages.ConfirmationViewed{})
|
||||||
|
|
||||||
|
// Callback to cart
|
||||||
|
|
||||||
|
// cartId := cart.CartId(checkoutId) // Assuming same
|
||||||
|
// s.cartClient.ApplyMutation(cartId, &messages.OrderCreated{OrderId: order.ID, Status: order.Status})
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
fmt.Fprintf(w, tpl, order.HTMLSnippet)
|
||||||
|
})
|
||||||
|
}
|
||||||
115
cmd/checkout/utils.go
Normal file
115
cmd/checkout/utils.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"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 {
|
||||||
|
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 ""
|
||||||
|
}
|
||||||
|
|
||||||
|
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(w http.ResponseWriter, r *http.Request, checkoutId checkout.CheckoutId) 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(w, r, id)
|
||||||
|
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(w http.ResponseWriter, r *http.Request, checkoutId checkout.CheckoutId) error {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, checkoutId checkout.CheckoutId) 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -357,6 +357,139 @@ spec:
|
|||||||
ports:
|
ports:
|
||||||
- name: web
|
- name: web
|
||||||
port: 8080
|
port: 8080
|
||||||
|
- name: internal
|
||||||
|
port: 8081
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: checkout-actor
|
||||||
|
arch: amd64
|
||||||
|
name: checkout-actor-x86
|
||||||
|
spec:
|
||||||
|
replicas: 3
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: checkout-actor
|
||||||
|
arch: amd64
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: checkout-actor
|
||||||
|
actor-pool: checkout
|
||||||
|
arch: amd64
|
||||||
|
spec:
|
||||||
|
affinity:
|
||||||
|
nodeAffinity:
|
||||||
|
requiredDuringSchedulingIgnoredDuringExecution:
|
||||||
|
nodeSelectorTerms:
|
||||||
|
- matchExpressions:
|
||||||
|
- key: kubernetes.io/arch
|
||||||
|
operator: NotIn
|
||||||
|
values:
|
||||||
|
- arm64
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
nfs:
|
||||||
|
path: /i-data/7a8af061/nfs/checkout-actor
|
||||||
|
server: 10.10.1.10
|
||||||
|
serviceAccountName: default
|
||||||
|
containers:
|
||||||
|
- image: registry.k6n.net/go-checkout-actor-amd64:latest
|
||||||
|
name: checkout-actor-amd64
|
||||||
|
imagePullPolicy: Always
|
||||||
|
lifecycle:
|
||||||
|
preStop:
|
||||||
|
exec:
|
||||||
|
command: ["sleep", "15"]
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
name: web
|
||||||
|
- containerPort: 8081
|
||||||
|
name: debug
|
||||||
|
- containerPort: 1337
|
||||||
|
name: rpc
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /livez
|
||||||
|
port: web
|
||||||
|
failureThreshold: 1
|
||||||
|
periodSeconds: 30
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /readyz
|
||||||
|
port: web
|
||||||
|
failureThreshold: 2
|
||||||
|
initialDelaySeconds: 2
|
||||||
|
periodSeconds: 50
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: "/data"
|
||||||
|
name: data
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: "768Mi"
|
||||||
|
requests:
|
||||||
|
memory: "70Mi"
|
||||||
|
cpu: "1200m"
|
||||||
|
env:
|
||||||
|
- name: TZ
|
||||||
|
value: "Europe/Stockholm"
|
||||||
|
- name: REDIS_ADDRESS
|
||||||
|
value: "10.10.3.18:6379"
|
||||||
|
- name: REDIS_PASSWORD
|
||||||
|
value: "slaskredis"
|
||||||
|
- name: OTEL_RESOURCE_ATTRIBUTES
|
||||||
|
value: "service.name=checkout,service.version=0.1.2"
|
||||||
|
- name: OTEL_EXPORTER_OTLP_ENDPOINT
|
||||||
|
value: "http://otel-debug-service.monitoring:4317"
|
||||||
|
- name: ADYEN_HMAC
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: adyen
|
||||||
|
key: HMAC
|
||||||
|
- name: ADYEN_API_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: adyen
|
||||||
|
key: API_KEY
|
||||||
|
- name: KLARNA_API_USERNAME
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: klarna-api-credentials
|
||||||
|
key: username
|
||||||
|
- name: KLARNA_API_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: klarna-api-credentials
|
||||||
|
key: password
|
||||||
|
- name: POD_IP
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: status.podIP
|
||||||
|
- name: AMQP_URL
|
||||||
|
value: "amqp://admin:12bananer@rabbitmq.dev:5672/"
|
||||||
|
- name: CART_INTERNAL_URL
|
||||||
|
value: "http://cart-actor:8081"
|
||||||
|
- name: POD_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.name
|
||||||
|
---
|
||||||
|
kind: Service
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: checkout-actor
|
||||||
|
annotations:
|
||||||
|
prometheus.io/port: "8081"
|
||||||
|
prometheus.io/scrape: "true"
|
||||||
|
prometheus.io/path: "/metrics"
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: checkout-actor
|
||||||
|
ports:
|
||||||
|
- name: web
|
||||||
|
port: 8080
|
||||||
---
|
---
|
||||||
kind: Service
|
kind: Service
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/control"
|
||||||
"go.opentelemetry.io/contrib/bridges/otelslog"
|
"go.opentelemetry.io/contrib/bridges/otelslog"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
@@ -145,7 +145,7 @@ func (s *ControlServer[V]) AnnounceExpiry(ctx context.Context, req *messages.Exp
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ControlPlane: Ping
|
// ControlPlane: Ping
|
||||||
func (s *ControlServer[V]) Ping(ctx context.Context, _ *messages.Empty) (*messages.PingReply, error) {
|
func (s *ControlServer[V]) Ping(ctx context.Context, req *messages.Empty) (*messages.PingReply, error) {
|
||||||
|
|
||||||
host := s.pool.Hostname()
|
host := s.pool.Hostname()
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@ func (s *ControlServer[V]) Negotiate(ctx context.Context, req *messages.Negotiat
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ControlPlane: GetCartIds (locally owned carts only)
|
// ControlPlane: GetCartIds (locally owned carts only)
|
||||||
func (s *ControlServer[V]) GetLocalActorIds(ctx context.Context, _ *messages.Empty) (*messages.ActorIdsReply, error) {
|
func (s *ControlServer[V]) GetLocalActorIds(ctx context.Context, req *messages.Empty) (*messages.ActorIdsReply, error) {
|
||||||
ctx, span := tracer.Start(ctx, "grpc_get_local_actor_ids")
|
ctx, span := tracer.Start(ctx, "grpc_get_local_actor_ids")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
ids := s.pool.GetLocalIds()
|
ids := s.pool.GetLocalIds()
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.k6n.net/go-cart-actor/pkg/messages"
|
cart_messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||||
|
control_plane_messages "git.k6n.net/go-cart-actor/proto/control"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
@@ -70,12 +71,12 @@ func TestApplyRequestWithMutations(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
client := messages.NewControlPlaneClient(conn)
|
client := control_plane_messages.NewControlPlaneClient(conn)
|
||||||
|
|
||||||
// Prepare ApplyRequest with multiple Any messages
|
// Prepare ApplyRequest with multiple Any messages
|
||||||
addItemAny, _ := anypb.New(&messages.AddItem{ItemId: 1, Quantity: 2})
|
addItemAny, _ := anypb.New(&cart_messages.AddItem{ItemId: 1, Quantity: 2})
|
||||||
removeItemAny, _ := anypb.New(&messages.RemoveItem{Id: 1})
|
removeItemAny, _ := anypb.New(&cart_messages.RemoveItem{Id: 1})
|
||||||
req := &messages.ApplyRequest{
|
req := &control_plane_messages.ApplyRequest{
|
||||||
Id: 123,
|
Id: 123,
|
||||||
Messages: []*anypb.Any{addItemAny, removeItemAny},
|
Messages: []*anypb.Any{addItemAny, removeItemAny},
|
||||||
}
|
}
|
||||||
@@ -95,10 +96,10 @@ func TestApplyRequestWithMutations(t *testing.T) {
|
|||||||
if len(pool.applied) != 2 {
|
if len(pool.applied) != 2 {
|
||||||
t.Errorf("expected 2 mutations applied, got %d", len(pool.applied))
|
t.Errorf("expected 2 mutations applied, got %d", len(pool.applied))
|
||||||
}
|
}
|
||||||
if addItem, ok := pool.applied[0].(*messages.AddItem); !ok || addItem.ItemId != 1 {
|
if addItem, ok := pool.applied[0].(*cart_messages.AddItem); !ok || addItem.ItemId != 1 {
|
||||||
t.Errorf("expected AddItem with ItemId=1, got %v", pool.applied[0])
|
t.Errorf("expected AddItem with ItemId=1, got %v", pool.applied[0])
|
||||||
}
|
}
|
||||||
if removeItem, ok := pool.applied[1].(*messages.RemoveItem); !ok || removeItem.Id != 1 {
|
if removeItem, ok := pool.applied[1].(*cart_messages.RemoveItem); !ok || removeItem.Id != 1 {
|
||||||
t.Errorf("expected RemoveItem with Id=1, got %v", pool.applied[1])
|
t.Errorf("expected RemoveItem with Id=1, got %v", pool.applied[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.Noop{}); 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.Noop{})
|
_, 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ package cart
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"slices"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
|
||||||
"git.k6n.net/go-cart-actor/pkg/voucher"
|
"git.k6n.net/go-cart-actor/pkg/voucher"
|
||||||
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
||||||
)
|
)
|
||||||
@@ -55,14 +53,6 @@ type CartItem struct {
|
|||||||
ReservationEndTime *time.Time `json:"reservationEndTime,omitempty"`
|
ReservationEndTime *time.Time `json:"reservationEndTime,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CartDelivery struct {
|
|
||||||
Id uint32 `json:"id"`
|
|
||||||
Provider string `json:"provider"`
|
|
||||||
Price Price `json:"price"`
|
|
||||||
Items []uint32 `json:"items"`
|
|
||||||
PickupPoint *messages.PickupPoint `json:"pickupPoint,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CartNotification struct {
|
type CartNotification struct {
|
||||||
LinkedId int `json:"id"`
|
LinkedId int `json:"id"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
@@ -84,103 +74,46 @@ type Notice struct {
|
|||||||
Code *string `json:"code,omitempty"`
|
Code *string `json:"code,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaymentStatus string
|
type CartPaymentStatus string
|
||||||
type CartPaymentStatus PaymentStatus
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PaymentStatusPending PaymentStatus = "pending"
|
|
||||||
PaymentStatusFailed PaymentStatus = "failed"
|
|
||||||
PaymentStatusSuccess PaymentStatus = "success"
|
|
||||||
CartPaymentStatusPending CartPaymentStatus = "pending"
|
CartPaymentStatusPending CartPaymentStatus = "pending"
|
||||||
CartPaymentStatusFailed CartPaymentStatus = "failed"
|
CartPaymentStatusFailed CartPaymentStatus = "failed"
|
||||||
CartPaymentStatusSuccess CartPaymentStatus = "success"
|
CartPaymentStatusSuccess CartPaymentStatus = "success"
|
||||||
CartPaymentStatusCancelled CartPaymentStatus = "partial"
|
CartPaymentStatusCancelled CartPaymentStatus = "partial"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CartPayment struct {
|
|
||||||
PaymentId string `json:"paymentId"`
|
|
||||||
Status PaymentStatus `json:"status"`
|
|
||||||
Amount int64 `json:"amount"`
|
|
||||||
Currency string `json:"currency"`
|
|
||||||
Provider string `json:"provider,omitempty"`
|
|
||||||
Method *string `json:"method,omitempty"`
|
|
||||||
Events []*PaymentEvent `json:"events,omitempty"`
|
|
||||||
ProcessorReference *string `json:"processorReference,omitempty"`
|
|
||||||
StartedAt *time.Time `json:"startedAt,omitempty"`
|
|
||||||
CompletedAt *time.Time `json:"completedAt,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PaymentEvent struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Data json.RawMessage `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *CartPayment) IsSettled() bool {
|
|
||||||
if p == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch p.Status {
|
|
||||||
case PaymentStatusSuccess:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Marking struct {
|
type Marking struct {
|
||||||
Type uint32 `json:"type"`
|
Type uint32 `json:"type"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GiftcardItem struct {
|
|
||||||
Id uint32 `json:"id"`
|
|
||||||
Value Price `json:"value"`
|
|
||||||
DeliveryDate string `json:"deliveryDate"`
|
|
||||||
Recipient string `json:"recipient"`
|
|
||||||
RecipientType string `json:"recipientType"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
DesignConfig json.RawMessage `json:"designConfig,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CartGrain struct {
|
type CartGrain struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
lastItemId uint32
|
lastItemId uint32
|
||||||
lastDeliveryId uint32
|
|
||||||
lastVoucherId uint32
|
lastVoucherId uint32
|
||||||
lastGiftcardId uint32
|
|
||||||
lastAccess time.Time
|
lastAccess time.Time
|
||||||
lastChange time.Time // unix seconds of last successful mutation (replay sets from event ts)
|
lastChange time.Time // unix seconds of last successful mutation (replay sets from event ts)
|
||||||
userId string
|
userId string
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
Language string `json:"language"`
|
||||||
Version uint `json:"version"`
|
Version uint `json:"version"`
|
||||||
InventoryReserved bool `json:"inventoryReserved"`
|
InventoryReserved bool `json:"inventoryReserved"`
|
||||||
Id CartId `json:"id"`
|
Id CartId `json:"id"`
|
||||||
Items []*CartItem `json:"items"`
|
Items []*CartItem `json:"items"`
|
||||||
Giftcards []*GiftcardItem `json:"giftcards,omitempty"`
|
|
||||||
TotalPrice *Price `json:"totalPrice"`
|
TotalPrice *Price `json:"totalPrice"`
|
||||||
TotalDiscount *Price `json:"totalDiscount"`
|
TotalDiscount *Price `json:"totalDiscount"`
|
||||||
Deliveries []*CartDelivery `json:"deliveries,omitempty"`
|
|
||||||
Processing bool `json:"processing"`
|
Processing bool `json:"processing"`
|
||||||
PaymentInProgress uint16 `json:"paymentInProgress"`
|
//PaymentInProgress uint16 `json:"paymentInProgress"`
|
||||||
OrderReference string `json:"orderReference,omitempty"`
|
OrderReference string `json:"orderReference,omitempty"`
|
||||||
PaymentStatus PaymentStatus `json:"paymentStatus,omitempty"`
|
|
||||||
PaidInFull bool `json:"paidInFull"`
|
|
||||||
Vouchers []*Voucher `json:"vouchers,omitempty"`
|
Vouchers []*Voucher `json:"vouchers,omitempty"`
|
||||||
Notifications []CartNotification `json:"cartNotification,omitempty"`
|
Notifications []CartNotification `json:"cartNotification,omitempty"`
|
||||||
SubscriptionDetails map[string]*SubscriptionDetails `json:"subscriptionDetails,omitempty"`
|
SubscriptionDetails map[string]*SubscriptionDetails `json:"subscriptionDetails,omitempty"`
|
||||||
PaymentDeclinedNotices []Notice `json:"paymentDeclinedNotices,omitempty"`
|
|
||||||
Payments []*CartPayment `json:"payments,omitempty"`
|
|
||||||
Confirmation *ConfirmationStatus `json:"confirmation,omitempty"`
|
|
||||||
//CheckoutOrderId string `json:"checkoutOrderId,omitempty"`
|
|
||||||
CheckoutStatus CartPaymentStatus `json:"checkoutStatus,omitempty"`
|
|
||||||
//CheckoutCountry string `json:"checkoutCountry,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfirmationStatus struct {
|
//CheckoutOrderId string `json:"checkoutOrderId,omitempty"`
|
||||||
Code *string `json:"code,omitempty"`
|
CheckoutStatus *CartPaymentStatus `json:"checkoutStatus,omitempty"`
|
||||||
ViewCount int `json:"viewCount"`
|
//CheckoutCountry string `json:"checkoutCountry,omitempty"`
|
||||||
LastViewedAt time.Time `json:"lastViewedAt"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Voucher struct {
|
type Voucher struct {
|
||||||
@@ -243,19 +176,14 @@ func (v *Voucher) AppliesTo(cart *CartGrain) ([]*CartItem, bool) {
|
|||||||
func NewCartGrain(id uint64, ts time.Time) *CartGrain {
|
func NewCartGrain(id uint64, ts time.Time) *CartGrain {
|
||||||
return &CartGrain{
|
return &CartGrain{
|
||||||
lastItemId: 0,
|
lastItemId: 0,
|
||||||
lastDeliveryId: 0,
|
|
||||||
lastVoucherId: 0,
|
lastVoucherId: 0,
|
||||||
lastGiftcardId: 0,
|
|
||||||
lastAccess: ts,
|
lastAccess: ts,
|
||||||
lastChange: ts,
|
lastChange: ts,
|
||||||
TotalDiscount: NewPrice(),
|
TotalDiscount: NewPrice(),
|
||||||
Vouchers: []*Voucher{},
|
Vouchers: []*Voucher{},
|
||||||
Deliveries: []*CartDelivery{},
|
|
||||||
Giftcards: []*GiftcardItem{},
|
|
||||||
Id: CartId(id),
|
Id: CartId(id),
|
||||||
Items: []*CartItem{},
|
Items: []*CartItem{},
|
||||||
TotalPrice: NewPrice(),
|
TotalPrice: NewPrice(),
|
||||||
Payments: []*CartPayment{},
|
|
||||||
SubscriptionDetails: make(map[string]*SubscriptionDetails),
|
SubscriptionDetails: make(map[string]*SubscriptionDetails),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,33 +222,6 @@ func (c *CartGrain) GetState() ([]byte, error) {
|
|||||||
return json.Marshal(c)
|
return json.Marshal(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CartGrain) ItemsWithDelivery() []uint32 {
|
|
||||||
ret := make([]uint32, 0, len(c.Items))
|
|
||||||
for _, item := range c.Items {
|
|
||||||
for _, delivery := range c.Deliveries {
|
|
||||||
for _, id := range delivery.Items {
|
|
||||||
if item.Id == id {
|
|
||||||
ret = append(ret, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CartGrain) ItemsWithoutDelivery() []uint32 {
|
|
||||||
ret := make([]uint32, 0, len(c.Items))
|
|
||||||
hasDelivery := c.ItemsWithDelivery()
|
|
||||||
for _, item := range c.Items {
|
|
||||||
found := slices.Contains(hasDelivery, item.Id)
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
ret = append(ret, item.Id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CartGrain) FindItemWithSku(sku string) (*CartItem, bool) {
|
func (c *CartGrain) FindItemWithSku(sku string) (*CartItem, bool) {
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
@@ -332,73 +233,6 @@ func (c *CartGrain) FindItemWithSku(sku string) (*CartItem, bool) {
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CartGrain) FindPayment(paymentId string) (*CartPayment, bool) {
|
|
||||||
if paymentId == "" {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
for _, payment := range c.Payments {
|
|
||||||
if payment != nil && payment.PaymentId == paymentId {
|
|
||||||
return payment, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CartGrain) SettledPayments() []*CartPayment {
|
|
||||||
if len(c.Payments) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
settled := make([]*CartPayment, 0, len(c.Payments))
|
|
||||||
for _, payment := range c.Payments {
|
|
||||||
if payment != nil && payment.IsSettled() {
|
|
||||||
settled = append(settled, payment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(settled) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return settled
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CartGrain) OpenPayments() []*CartPayment {
|
|
||||||
if len(c.Payments) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
pending := make([]*CartPayment, 0, len(c.Payments))
|
|
||||||
for _, payment := range c.Payments {
|
|
||||||
if payment == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !payment.IsSettled() {
|
|
||||||
pending = append(pending, payment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(pending) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return pending
|
|
||||||
}
|
|
||||||
|
|
||||||
// func (c *CartGrain) Apply(content proto.Message, isReplay bool) (*CartGrain, error) {
|
|
||||||
|
|
||||||
// updated, err := ApplyRegistered(c, content)
|
|
||||||
// if err != nil {
|
|
||||||
// if err == ErrMutationNotRegistered {
|
|
||||||
// return nil, fmt.Errorf("unsupported mutation type %T (not registered)", content)
|
|
||||||
// }
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Sliding TTL: update lastChange only for non-replay successful mutations.
|
|
||||||
// if updated != nil && !isReplay {
|
|
||||||
// c.lastChange = time.Now()
|
|
||||||
// c.lastAccess = time.Now()
|
|
||||||
// go AppendCartEvent(c.Id, content)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return updated, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
func (c *CartGrain) UpdateTotals() {
|
func (c *CartGrain) UpdateTotals() {
|
||||||
c.TotalPrice = NewPrice()
|
c.TotalPrice = NewPrice()
|
||||||
c.TotalDiscount = NewPrice()
|
c.TotalDiscount = NewPrice()
|
||||||
@@ -423,12 +257,7 @@ func (c *CartGrain) UpdateTotals() {
|
|||||||
c.TotalPrice.Add(*rowTotal)
|
c.TotalPrice.Add(*rowTotal)
|
||||||
|
|
||||||
}
|
}
|
||||||
for _, delivery := range c.Deliveries {
|
|
||||||
c.TotalPrice.Add(delivery.Price)
|
|
||||||
}
|
|
||||||
for _, giftcard := range c.Giftcards {
|
|
||||||
c.TotalPrice.Add(giftcard.Value)
|
|
||||||
}
|
|
||||||
for _, voucher := range c.Vouchers {
|
for _, voucher := range c.Vouchers {
|
||||||
_, ok := voucher.AppliesTo(c)
|
_, ok := voucher.AppliesTo(c)
|
||||||
voucher.Applied = false
|
voucher.Applied = false
|
||||||
|
|||||||
@@ -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/pkg/messages"
|
|
||||||
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -57,88 +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(context.InitializeCheckout, func() *messages.InitializeCheckout {
|
actor.NewMutation(RemoveLineItemMarking),
|
||||||
return &messages.InitializeCheckout{}
|
actor.NewMutation(SubscriptionAdded),
|
||||||
}),
|
// actor.NewMutation(SubscriptionRemoved),
|
||||||
actor.NewMutation(OrderCreated, func() *messages.OrderCreated {
|
|
||||||
return &messages.OrderCreated{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(RemoveDelivery, func() *messages.RemoveDelivery {
|
|
||||||
return &messages.RemoveDelivery{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(SetDelivery, func() *messages.SetDelivery {
|
|
||||||
return &messages.SetDelivery{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(SetPickupPoint, func() *messages.SetPickupPoint {
|
|
||||||
return &messages.SetPickupPoint{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(ClearCart, func() *messages.ClearCartRequest {
|
|
||||||
return &messages.ClearCartRequest{}
|
|
||||||
}),
|
|
||||||
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(context.InventoryReserved, func() *messages.InventoryReserved {
|
|
||||||
return &messages.InventoryReserved{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(PreConditionFailed, func() *messages.PreConditionFailed {
|
|
||||||
return &messages.PreConditionFailed{}
|
|
||||||
}),
|
|
||||||
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{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(PaymentStarted, func() *messages.PaymentStarted {
|
|
||||||
return &messages.PaymentStarted{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(PaymentCompleted, func() *messages.PaymentCompleted {
|
|
||||||
return &messages.PaymentCompleted{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(PaymentDeclined, func() *messages.PaymentDeclined {
|
|
||||||
return &messages.PaymentDeclined{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(PaymentEventHandler, func() *messages.PaymentEvent {
|
|
||||||
return &messages.PaymentEvent{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(ConfirmationViewed, func() *messages.ConfirmationViewed {
|
|
||||||
return &messages.ConfirmationViewed{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(CreateCheckoutOrder, func() *messages.CreateCheckoutOrder {
|
|
||||||
return &messages.CreateCheckoutOrder{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(AddGiftcard, func() *messages.AddGiftcard {
|
|
||||||
return &messages.AddGiftcard{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(RemoveGiftcard, func() *messages.RemoveGiftcard {
|
|
||||||
return &messages.RemoveGiftcard{}
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
return reg
|
return reg
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package cart
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// helper to create a cart grain with items and deliveries
|
|
||||||
func newTestCart() *CartGrain {
|
|
||||||
return &CartGrain{Items: []*CartItem{}, Deliveries: []*CartDelivery{}, Vouchers: []*Voucher{}, Notifications: []CartNotification{}}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCartGrainUpdateTotalsBasic(t *testing.T) {
|
|
||||||
c := newTestCart()
|
|
||||||
// Item1 price 1250 (ex 1000 vat 250) org price higher -> discount 200 per unit
|
|
||||||
item1Price := Price{IncVat: 1250, VatRates: map[float32]int64{25: 250}}
|
|
||||||
item1Org := &Price{IncVat: 1500, VatRates: map[float32]int64{25: 300}}
|
|
||||||
item2Price := Price{IncVat: 2000, VatRates: map[float32]int64{25: 400}}
|
|
||||||
c.Items = []*CartItem{
|
|
||||||
{Id: 1, Price: item1Price, OrgPrice: item1Org, Quantity: 2},
|
|
||||||
{Id: 2, Price: item2Price, OrgPrice: &item2Price, Quantity: 1},
|
|
||||||
}
|
|
||||||
deliveryPrice := Price{IncVat: 4900, VatRates: map[float32]int64{25: 980}}
|
|
||||||
c.Deliveries = []*CartDelivery{{Id: 1, Price: deliveryPrice, Items: []uint32{1, 2}}}
|
|
||||||
|
|
||||||
c.UpdateTotals()
|
|
||||||
|
|
||||||
// Expected totals: sum inc vat of items * qty plus delivery
|
|
||||||
// item1 total inc = 1250*2 = 2500
|
|
||||||
// item2 total inc = 2000*1 = 2000
|
|
||||||
// delivery inc = 4900
|
|
||||||
expectedInc := int64(2500 + 2000 + 4900)
|
|
||||||
if c.TotalPrice.IncVat != expectedInc {
|
|
||||||
t.Fatalf("TotalPrice IncVat expected %d got %d", expectedInc, c.TotalPrice.IncVat)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discount: current implementation computes (OrgPrice - Price) ignoring quantity -> 1500-1250=250
|
|
||||||
if c.TotalDiscount.IncVat != 500 {
|
|
||||||
t.Fatalf("TotalDiscount expected 500 got %d", c.TotalDiscount.IncVat)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCartGrainUpdateTotalsNoItems(t *testing.T) {
|
|
||||||
c := newTestCart()
|
|
||||||
c.UpdateTotals()
|
|
||||||
if c.TotalPrice.IncVat != 0 || c.TotalDiscount.IncVat != 0 {
|
|
||||||
t.Fatalf("expected zero totals got %+v", c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package cart
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
func AddGiftcard(grain *CartGrain, req *messages.AddGiftcard) error {
|
|
||||||
if req.Giftcard == nil {
|
|
||||||
return fmt.Errorf("giftcard cannot be nil")
|
|
||||||
}
|
|
||||||
if req.Giftcard.Value <= 0 {
|
|
||||||
return fmt.Errorf("giftcard value must be positive")
|
|
||||||
}
|
|
||||||
if grain.PaymentInProgress > 0 {
|
|
||||||
return ErrPaymentInProgress
|
|
||||||
}
|
|
||||||
grain.lastGiftcardId++
|
|
||||||
designConfig := json.RawMessage{}
|
|
||||||
if req.Giftcard.DesignConfig != nil {
|
|
||||||
// Convert Any to RawMessage
|
|
||||||
data, err := proto.Marshal(req.Giftcard.DesignConfig)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal designConfig: %w", err)
|
|
||||||
}
|
|
||||||
designConfig = data
|
|
||||||
}
|
|
||||||
value := NewPriceFromIncVat(req.Giftcard.Value, 25) // Assuming 25% tax; adjust as needed
|
|
||||||
item := &GiftcardItem{
|
|
||||||
Id: grain.lastGiftcardId,
|
|
||||||
Value: *value,
|
|
||||||
DeliveryDate: req.Giftcard.DeliveryDate,
|
|
||||||
Recipient: req.Giftcard.Recipient,
|
|
||||||
RecipientType: req.Giftcard.RecipientType,
|
|
||||||
Message: req.Giftcard.Message,
|
|
||||||
DesignConfig: designConfig,
|
|
||||||
}
|
|
||||||
grain.Giftcards = append(grain.Giftcards, item)
|
|
||||||
grain.UpdateTotals()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
cart_messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ import (
|
|||||||
// must keep this handler in sync.
|
// must keep this handler in sync.
|
||||||
var ErrPaymentInProgress = errors.New("payment in progress")
|
var ErrPaymentInProgress = errors.New("payment in progress")
|
||||||
|
|
||||||
func (c *CartMutationContext) AddItem(g *CartGrain, m *messages.AddItem) error {
|
func (c *CartMutationContext) AddItem(g *CartGrain, m *cart_messages.AddItem) error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return fmt.Errorf("AddItem: nil payload")
|
return fmt.Errorf("AddItem: nil payload")
|
||||||
@@ -34,9 +34,6 @@ func (c *CartMutationContext) AddItem(g *CartGrain, m *messages.AddItem) error {
|
|||||||
if m.Quantity < 1 {
|
if m.Quantity < 1 {
|
||||||
return fmt.Errorf("AddItem: invalid quantity %d", m.Quantity)
|
return fmt.Errorf("AddItem: invalid quantity %d", m.Quantity)
|
||||||
}
|
}
|
||||||
if g.PaymentInProgress > 0 {
|
|
||||||
return ErrPaymentInProgress
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge with any existing item having same SKU and matching StoreId (including both nil).
|
// Merge with any existing item having same SKU and matching StoreId (including both nil).
|
||||||
for _, existing := range g.Items {
|
for _, existing := range g.Items {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"git.k6n.net/go-cart-actor/pkg/actor"
|
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||||
"git.k6n.net/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RemoveVoucher(g *CartGrain, m *messages.RemoveVoucher) error {
|
func RemoveVoucher(g *CartGrain, m *messages.RemoveVoucher) error {
|
||||||
@@ -15,7 +15,7 @@ func RemoveVoucher(g *CartGrain, m *messages.RemoveVoucher) error {
|
|||||||
StatusCode: 400,
|
StatusCode: 400,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if g.PaymentInProgress > 0 {
|
if g.CheckoutStatus != nil {
|
||||||
return ErrPaymentInProgress
|
return ErrPaymentInProgress
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,10 +45,6 @@ func AddVoucher(g *CartGrain, m *messages.AddVoucher) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if g.PaymentInProgress > 0 {
|
|
||||||
return ErrPaymentInProgress
|
|
||||||
}
|
|
||||||
|
|
||||||
if slices.ContainsFunc(g.Vouchers, func(v *Voucher) bool {
|
if slices.ContainsFunc(g.Vouchers, func(v *Voucher) bool {
|
||||||
return v.Code == m.Code
|
return v.Code == m.Code
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mutation_change_quantity.go
|
// mutation_change_quantity.go
|
||||||
@@ -32,9 +32,7 @@ func (c *CartMutationContext) ChangeQuantity(g *CartGrain, m *messages.ChangeQua
|
|||||||
if m == nil {
|
if m == nil {
|
||||||
return fmt.Errorf("ChangeQuantity: nil payload")
|
return fmt.Errorf("ChangeQuantity: nil payload")
|
||||||
}
|
}
|
||||||
if g.PaymentInProgress > 0 {
|
|
||||||
return ErrPaymentInProgress
|
|
||||||
}
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
foundIndex := -1
|
foundIndex := -1
|
||||||
|
|||||||
26
pkg/cart/mutation_clear_cart.go
Normal file
26
pkg/cart/mutation_clear_cart.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package cart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ClearCart(g *CartGrain, m *messages.ClearCartRequest) error {
|
||||||
|
if m == nil {
|
||||||
|
return fmt.Errorf("ClearCart: nil payload")
|
||||||
|
}
|
||||||
|
if g.CheckoutStatus != nil {
|
||||||
|
return fmt.Errorf("ClearCart: cart is in checkout")
|
||||||
|
}
|
||||||
|
// Clear items, vouchers, etc., but keep userId, etc.
|
||||||
|
g.Items = g.Items[:0]
|
||||||
|
g.Vouchers = g.Vouchers[:0]
|
||||||
|
g.Notifications = g.Notifications[:0]
|
||||||
|
g.OrderReference = ""
|
||||||
|
g.Processing = false
|
||||||
|
|
||||||
|
// g.InventoryReserved = false maybe should release inventory
|
||||||
|
g.UpdateTotals()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package cart
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ConfirmationViewed(grain *CartGrain, req *messages.ConfirmationViewed) error {
|
|
||||||
if grain.Confirmation == nil {
|
|
||||||
grain.Confirmation = &ConfirmationStatus{
|
|
||||||
ViewCount: 1,
|
|
||||||
LastViewedAt: time.Now(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
grain.Confirmation.ViewCount++
|
|
||||||
grain.Confirmation.LastViewedAt = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package cart
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CreateCheckoutOrder(grain *CartGrain, req *messages.CreateCheckoutOrder) error {
|
|
||||||
if len(grain.Items) == 0 {
|
|
||||||
return errors.New("cannot checkout empty cart")
|
|
||||||
}
|
|
||||||
if req.Terms != "accepted" {
|
|
||||||
return errors.New("terms must be accepted")
|
|
||||||
}
|
|
||||||
// Validate other fields as needed
|
|
||||||
//grain.CheckoutOrderId = uuid.New().String()
|
|
||||||
grain.CheckoutStatus = "pending"
|
|
||||||
//grain.CheckoutCountry = req.Country
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package cart
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
|
||||||
)
|
|
||||||
|
|
||||||
// mutation_initialize_checkout.go
|
|
||||||
//
|
|
||||||
// Registers the InitializeCheckout mutation.
|
|
||||||
// This mutation is invoked AFTER an external Klarna checkout session
|
|
||||||
// has been successfully created or updated. It persists the Klarna
|
|
||||||
// order reference / status and marks the cart as having a payment in progress.
|
|
||||||
//
|
|
||||||
// Behavior:
|
|
||||||
// - Sets OrderReference to the Klarna order ID (overwriting if already set).
|
|
||||||
// - Sets PaymentStatus to the current Klarna status.
|
|
||||||
// - Sets / updates PaymentInProgress flag.
|
|
||||||
// - Does NOT alter pricing or line items (so no totals recalculation).
|
|
||||||
//
|
|
||||||
// Validation:
|
|
||||||
// - Returns an error if payload is nil.
|
|
||||||
// - Returns an error if orderId is empty (integrity guard).
|
|
||||||
//
|
|
||||||
// Concurrency:
|
|
||||||
// - Relies on upstream mutation serialization for a single grain. If
|
|
||||||
// parallel checkout attempts are possible, add higher-level guards
|
|
||||||
// (e.g. reject if PaymentInProgress already true unless reusing
|
|
||||||
// the same OrderReference).
|
|
||||||
|
|
||||||
func (c *CartMutationContext) InitializeCheckout(g *CartGrain, m *messages.InitializeCheckout) error {
|
|
||||||
if m == nil {
|
|
||||||
return fmt.Errorf("InitializeCheckout: nil payload")
|
|
||||||
}
|
|
||||||
if m.OrderId == "" {
|
|
||||||
return fmt.Errorf("InitializeCheckout: missing orderId")
|
|
||||||
}
|
|
||||||
ctx := context.Background()
|
|
||||||
now := time.Now()
|
|
||||||
for _, item := range g.Items {
|
|
||||||
if item.ReservationEndTime != nil {
|
|
||||||
if now.After(*item.ReservationEndTime) {
|
|
||||||
endTime, err := c.ReserveItem(ctx, g.Id, item.Sku, item.StoreId, item.Quantity)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
item.ReservationEndTime = endTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
g.OrderReference = m.OrderId
|
|
||||||
//g.PaymentStatus = m.Status
|
|
||||||
//g.PaymentInProgress = m.PaymentInProgress
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package cart
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.k6n.net/go-cart-actor/pkg/messages"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *CartMutationContext) InventoryReserved(g *CartGrain, m *messages.InventoryReserved) error {
|
|
||||||
for _, item := range g.Items {
|
|
||||||
if item.ReservationEndTime != nil && item.ReservationEndTime.After(time.Now()) {
|
|
||||||
err := c.ReleaseItem(context.Background(), g.Id, item.Sku, item.StoreId)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("unable to release item reservation")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
g.InventoryReserved = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ package cart
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LineItemMarking(grain *CartGrain, req *messages.LineItemMarking) error {
|
func LineItemMarking(grain *CartGrain, req *messages.LineItemMarking) error {
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
package cart
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PaymentStarted registers the beginning of a payment attempt for a cart.
|
|
||||||
// It either upserts the payment entry (based on paymentId) or creates a new one,
|
|
||||||
// marks the cart as having an in-progress payment, and recalculates the PaidInFull flag.
|
|
||||||
func PaymentCompleted(grain *CartGrain, msg *messages.PaymentCompleted) error {
|
|
||||||
if msg == nil {
|
|
||||||
return fmt.Errorf("PaymentStarted: nil payload")
|
|
||||||
}
|
|
||||||
paymentId := msg.PaymentId
|
|
||||||
payment, found := grain.FindPayment(paymentId)
|
|
||||||
if !found {
|
|
||||||
return fmt.Errorf("PaymentStarted: payment not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
payment.ProcessorReference = msg.ProcessorReference
|
|
||||||
payment.Status = PaymentStatusSuccess
|
|
||||||
payment.Amount = msg.Amount
|
|
||||||
payment.Currency = msg.Currency
|
|
||||||
payment.CompletedAt = asPointer(time.Now())
|
|
||||||
|
|
||||||
// maybe update cart status
|
|
||||||
grain.PaymentInProgress--
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package cart
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
|
||||||
)
|
|
||||||
|
|
||||||
func asPointer[T any](value T) *T {
|
|
||||||
return &value
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrPaymentNotFound = errors.New("payment not found")
|
|
||||||
|
|
||||||
func PaymentDeclined(grain *CartGrain, req *messages.PaymentDeclined) error {
|
|
||||||
|
|
||||||
payment, found := grain.FindPayment(req.PaymentId)
|
|
||||||
if !found {
|
|
||||||
return ErrPaymentNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
payment.CompletedAt = asPointer(time.Now())
|
|
||||||
payment.Status = PaymentStatusFailed
|
|
||||||
grain.PaymentInProgress--
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package cart
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
|
||||||
)
|
|
||||||
|
|
||||||
func PaymentEventHandler(grain *CartGrain, req *messages.PaymentEvent) error {
|
|
||||||
|
|
||||||
payment, found := grain.FindPayment(req.PaymentId)
|
|
||||||
if !found {
|
|
||||||
return ErrPaymentNotFound
|
|
||||||
}
|
|
||||||
metaBytes := req.Data.GetValue()
|
|
||||||
payment.Events = append(payment.Events, &PaymentEvent{
|
|
||||||
Name: req.Name,
|
|
||||||
Success: req.Success,
|
|
||||||
Data: json.RawMessage(metaBytes),
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package cart
|
|
||||||
|
|
||||||
import messages "git.k6n.net/go-cart-actor/pkg/messages"
|
|
||||||
|
|
||||||
func PreConditionFailed(g *CartGrain, m *messages.PreConditionFailed) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package cart
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RemoveGiftcard(grain *CartGrain, req *messages.RemoveGiftcard) error {
|
|
||||||
if grain.PaymentInProgress > 0 {
|
|
||||||
return ErrPaymentInProgress
|
|
||||||
}
|
|
||||||
for i, item := range grain.Giftcards {
|
|
||||||
if item.Id == req.Id {
|
|
||||||
grain.Giftcards = append(grain.Giftcards[:i], grain.Giftcards[i+1:]...)
|
|
||||||
grain.UpdateTotals()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("giftcard with ID %d not found", req.Id)
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mutation_remove_item.go
|
// mutation_remove_item.go
|
||||||
@@ -29,9 +29,7 @@ func (c *CartMutationContext) RemoveItem(g *CartGrain, m *messages.RemoveItem) e
|
|||||||
if m == nil {
|
if m == nil {
|
||||||
return fmt.Errorf("RemoveItem: nil payload")
|
return fmt.Errorf("RemoveItem: nil payload")
|
||||||
}
|
}
|
||||||
if g.PaymentInProgress > 0 {
|
|
||||||
return ErrPaymentInProgress
|
|
||||||
}
|
|
||||||
targetID := uint32(m.Id)
|
targetID := uint32(m.Id)
|
||||||
|
|
||||||
index := -1
|
index := -1
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package cart
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RemoveLineItemMarking(grain *CartGrain, req *messages.RemoveLineItemMarking) error {
|
func RemoveLineItemMarking(grain *CartGrain, req *messages.RemoveLineItemMarking) error {
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
package cart
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"slices"
|
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
|
||||||
)
|
|
||||||
|
|
||||||
// mutation_set_delivery.go
|
|
||||||
//
|
|
||||||
// Registers the SetDelivery mutation.
|
|
||||||
//
|
|
||||||
// Semantics (mirrors legacy switch logic):
|
|
||||||
// - If the payload specifies an explicit list of item IDs (payload.Items):
|
|
||||||
// - Each referenced cart line must exist.
|
|
||||||
// - None of the referenced items may already belong to a delivery.
|
|
||||||
// - Only those items are associated with the new delivery.
|
|
||||||
// - If payload.Items is empty:
|
|
||||||
// - All items currently without any delivery are associated with the new delivery.
|
|
||||||
// - A new delivery line is created with:
|
|
||||||
// - Auto-incremented delivery ID (cart-local)
|
|
||||||
// - Provider from payload
|
|
||||||
// - Fixed price (currently hard-coded: 4900 minor units) – adjust as needed
|
|
||||||
// - Optional PickupPoint copied from payload
|
|
||||||
// - Cart totals are recalculated (WithTotals)
|
|
||||||
//
|
|
||||||
// Error cases:
|
|
||||||
// - Referenced item does not exist
|
|
||||||
// - Referenced item already has a delivery
|
|
||||||
// - No items qualify (resulting association set empty) -> returns error (prevents creating empty delivery)
|
|
||||||
//
|
|
||||||
// Concurrency:
|
|
||||||
// - Uses g.mu to protect lastDeliveryId increment and append to Deliveries slice.
|
|
||||||
// Item scans are read-only and performed outside the lock for simplicity;
|
|
||||||
// if stricter guarantees are needed, widen the lock section.
|
|
||||||
//
|
|
||||||
// Future extension points:
|
|
||||||
// - Variable delivery pricing (based on weight, distance, provider, etc.)
|
|
||||||
// - Validation of provider codes
|
|
||||||
// - Multi-currency delivery pricing
|
|
||||||
|
|
||||||
func SetDelivery(g *CartGrain, m *messages.SetDelivery) error {
|
|
||||||
if m == nil {
|
|
||||||
return fmt.Errorf("SetDelivery: nil payload")
|
|
||||||
}
|
|
||||||
if m.Provider == "" {
|
|
||||||
return fmt.Errorf("SetDelivery: provider is empty")
|
|
||||||
}
|
|
||||||
if g.PaymentInProgress > 0 {
|
|
||||||
return ErrPaymentInProgress
|
|
||||||
}
|
|
||||||
|
|
||||||
withDelivery := g.ItemsWithDelivery()
|
|
||||||
targetItems := make([]uint32, 0)
|
|
||||||
|
|
||||||
if len(m.Items) == 0 {
|
|
||||||
// Use every item currently without a delivery
|
|
||||||
targetItems = append(targetItems, g.ItemsWithoutDelivery()...)
|
|
||||||
} else {
|
|
||||||
// Validate explicit list
|
|
||||||
for _, id64 := range m.Items {
|
|
||||||
id := uint32(id64)
|
|
||||||
found := false
|
|
||||||
for _, it := range g.Items {
|
|
||||||
if it.Id == id {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return fmt.Errorf("SetDelivery: item id %d not found", id)
|
|
||||||
}
|
|
||||||
if slices.Contains(withDelivery, id) {
|
|
||||||
return fmt.Errorf("SetDelivery: item id %d already has a delivery", id)
|
|
||||||
}
|
|
||||||
targetItems = append(targetItems, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(targetItems) == 0 {
|
|
||||||
return fmt.Errorf("SetDelivery: no eligible items to attach")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append new delivery
|
|
||||||
g.mu.Lock()
|
|
||||||
g.lastDeliveryId++
|
|
||||||
newId := g.lastDeliveryId
|
|
||||||
g.Deliveries = append(g.Deliveries, &CartDelivery{
|
|
||||||
Id: newId,
|
|
||||||
Provider: m.Provider,
|
|
||||||
PickupPoint: m.PickupPoint,
|
|
||||||
Price: *NewPriceFromIncVat(4900, 25.0),
|
|
||||||
Items: targetItems,
|
|
||||||
})
|
|
||||||
g.mu.Unlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ package cart
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetUserId(grain *CartGrain, req *messages.SetUserId) error {
|
func SetUserId(grain *CartGrain, req *messages.SetUserId) error {
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ package cart
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SubscriptionAdded(grain *CartGrain, req *messages.SubscriptionAdded) error {
|
func SubscriptionAdded(grain *CartGrain, req *messages.SubscriptionAdded) error {
|
||||||
if grain.PaymentInProgress > 0 {
|
|
||||||
return ErrPaymentInProgress
|
|
||||||
}
|
|
||||||
for i, item := range grain.Items {
|
for i, item := range grain.Items {
|
||||||
if item.Id == req.ItemId {
|
if item.Id == req.ItemId {
|
||||||
grain.Items[i].SubscriptionDetailsId = req.DetailsId
|
grain.Items[i].SubscriptionDetailsId = req.DetailsId
|
||||||
|
|||||||
@@ -1,809 +0,0 @@
|
|||||||
package cart
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
"google.golang.org/protobuf/types/known/anypb"
|
|
||||||
|
|
||||||
"git.k6n.net/go-cart-actor/pkg/actor"
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ----------------------
|
|
||||||
// Helper constructors
|
|
||||||
// ----------------------
|
|
||||||
|
|
||||||
func newTestGrain() *CartGrain {
|
|
||||||
return NewCartGrain(123, time.Now())
|
|
||||||
}
|
|
||||||
|
|
||||||
type MockReservationService struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockReservationService) ReserveForCart(ctx context.Context, req inventory.CartReserveRequest) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockReservationService) ReleaseForCart(ctx context.Context, sku inventory.SKU, locationID inventory.LocationID, cartID inventory.CartID) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockReservationService) GetAvailableInventory(ctx context.Context, sku inventory.SKU, locationID inventory.LocationID) (int64, error) {
|
|
||||||
return 1000, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockReservationService) GetReservationExpiry(ctx context.Context, sku inventory.SKU, locationID inventory.LocationID, cartID inventory.CartID) (time.Time, error) {
|
|
||||||
return time.Time{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockReservationService) GetReservationStatus(ctx context.Context, sku inventory.SKU, locationID inventory.LocationID, cartID inventory.CartID) (*inventory.ReservationStatus, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockReservationService) GetReservationSummary(ctx context.Context, sku inventory.SKU, locationID inventory.LocationID) (*inventory.ReservationSummary, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRegistry() actor.MutationRegistry {
|
|
||||||
cartCtx := &CartMutationContext{
|
|
||||||
reservationService: &MockReservationService{},
|
|
||||||
}
|
|
||||||
return NewCartMultationRegistry(cartCtx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func msgAddItem(sku string, price int64, qty int32, storePtr *string) *messages.AddItem {
|
|
||||||
return &messages.AddItem{
|
|
||||||
Sku: sku,
|
|
||||||
Price: price,
|
|
||||||
Quantity: qty,
|
|
||||||
// Tax left 0 -> handler uses default 25%
|
|
||||||
StoreId: storePtr,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func msgChangeQty(id uint32, qty int32) *messages.ChangeQuantity {
|
|
||||||
return &messages.ChangeQuantity{Id: id, Quantity: qty}
|
|
||||||
}
|
|
||||||
|
|
||||||
func msgRemoveItem(id uint32) *messages.RemoveItem {
|
|
||||||
return &messages.RemoveItem{Id: id}
|
|
||||||
}
|
|
||||||
|
|
||||||
func msgSetDelivery(provider string, items ...uint32) *messages.SetDelivery {
|
|
||||||
uitems := make([]uint32, len(items))
|
|
||||||
copy(uitems, items)
|
|
||||||
return &messages.SetDelivery{Provider: provider, Items: uitems}
|
|
||||||
}
|
|
||||||
|
|
||||||
func msgSetPickupPoint(deliveryId uint32, id string) *messages.SetPickupPoint {
|
|
||||||
return &messages.SetPickupPoint{
|
|
||||||
DeliveryId: deliveryId,
|
|
||||||
Id: id,
|
|
||||||
Name: ptr("Pickup"),
|
|
||||||
Address: ptr("Street 1"),
|
|
||||||
City: ptr("Town"),
|
|
||||||
Zip: ptr("12345"),
|
|
||||||
Country: ptr("SE"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func msgClearCart() *messages.ClearCartRequest {
|
|
||||||
return &messages.ClearCartRequest{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func msgAddVoucher(code string, value int64, rules ...string) *messages.AddVoucher {
|
|
||||||
return &messages.AddVoucher{Code: code, Value: value, VoucherRules: rules}
|
|
||||||
}
|
|
||||||
|
|
||||||
func msgRemoveVoucher(id uint32) *messages.RemoveVoucher {
|
|
||||||
return &messages.RemoveVoucher{Id: id}
|
|
||||||
}
|
|
||||||
|
|
||||||
func msgInitializeCheckout(orderId, status string, inProgress bool) *messages.InitializeCheckout {
|
|
||||||
return &messages.InitializeCheckout{OrderId: orderId, Status: status, PaymentInProgress: inProgress}
|
|
||||||
}
|
|
||||||
|
|
||||||
func msgOrderCreated(orderId, status string) *messages.OrderCreated {
|
|
||||||
return &messages.OrderCreated{OrderId: orderId, Status: status}
|
|
||||||
}
|
|
||||||
|
|
||||||
func msgSetUserId(userId string) *messages.SetUserId {
|
|
||||||
return &messages.SetUserId{UserId: userId}
|
|
||||||
}
|
|
||||||
|
|
||||||
func msgLineItemMarking(id uint32, typ uint32, marking string) *messages.LineItemMarking {
|
|
||||||
return &messages.LineItemMarking{Id: id, Type: typ, Marking: marking}
|
|
||||||
}
|
|
||||||
|
|
||||||
func msgRemoveLineItemMarking(id uint32) *messages.RemoveLineItemMarking {
|
|
||||||
return &messages.RemoveLineItemMarking{Id: id}
|
|
||||||
}
|
|
||||||
|
|
||||||
func msgSubscriptionAdded(itemId uint32, detailsId, orderRef string) *messages.SubscriptionAdded {
|
|
||||||
return &messages.SubscriptionAdded{ItemId: itemId, DetailsId: detailsId, OrderReference: orderRef}
|
|
||||||
}
|
|
||||||
|
|
||||||
// func msgPaymentDeclined(message, code string) *messages.PaymentDeclined {
|
|
||||||
// return &messages.PaymentDeclined{Message: message, Code: &code}
|
|
||||||
// }
|
|
||||||
|
|
||||||
func msgConfirmationViewed() *messages.ConfirmationViewed {
|
|
||||||
return &messages.ConfirmationViewed{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func msgCreateCheckoutOrder(terms, country string) *messages.CreateCheckoutOrder {
|
|
||||||
return &messages.CreateCheckoutOrder{Terms: terms, Country: country}
|
|
||||||
}
|
|
||||||
|
|
||||||
func msgAddGiftcard(value int64, deliveryDate, recipient, recipientType, message string, designConfig *anypb.Any) *messages.AddGiftcard {
|
|
||||||
return &messages.AddGiftcard{
|
|
||||||
Giftcard: &messages.GiftcardItem{
|
|
||||||
Value: value,
|
|
||||||
DeliveryDate: deliveryDate,
|
|
||||||
Recipient: recipient,
|
|
||||||
RecipientType: recipientType,
|
|
||||||
Message: message,
|
|
||||||
DesignConfig: designConfig,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func msgRemoveGiftcard(id uint32) *messages.RemoveGiftcard {
|
|
||||||
return &messages.RemoveGiftcard{Id: id}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ptr[T any](v T) *T { return &v }
|
|
||||||
|
|
||||||
// ----------------------
|
|
||||||
// Apply helpers
|
|
||||||
// ----------------------
|
|
||||||
|
|
||||||
func applyOne(t *testing.T, reg actor.MutationRegistry, g *CartGrain, msg proto.Message) actor.ApplyResult {
|
|
||||||
t.Helper()
|
|
||||||
results, err := reg.Apply(context.Background(), g, msg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected registry-level error applying %T: %v", msg, err)
|
|
||||||
}
|
|
||||||
if len(results) != 1 {
|
|
||||||
t.Fatalf("expected exactly one ApplyResult, got %d", len(results))
|
|
||||||
}
|
|
||||||
return results[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expect success (nil error inside ApplyResult).
|
|
||||||
func applyOK(t *testing.T, reg actor.MutationRegistry, g *CartGrain, msg proto.Message) {
|
|
||||||
t.Helper()
|
|
||||||
res := applyOne(t, reg, g, msg)
|
|
||||||
if res.Error != nil {
|
|
||||||
t.Fatalf("expected mutation %s (%T) to succeed, got error: %v", res.Type, msg, res.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expect an error matching substring.
|
|
||||||
func applyErrorContains(t *testing.T, reg actor.MutationRegistry, g *CartGrain, msg proto.Message, substr string) {
|
|
||||||
t.Helper()
|
|
||||||
res := applyOne(t, reg, g, msg)
|
|
||||||
if res.Error == nil {
|
|
||||||
t.Fatalf("expected error applying %T, got nil", msg)
|
|
||||||
}
|
|
||||||
if substr != "" && !strings.Contains(res.Error.Error(), substr) {
|
|
||||||
t.Fatalf("error mismatch, want substring %q got %q", substr, res.Error.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------
|
|
||||||
// Tests
|
|
||||||
// ----------------------
|
|
||||||
|
|
||||||
func TestMutationRegistryCoverage(t *testing.T) {
|
|
||||||
reg := newRegistry()
|
|
||||||
|
|
||||||
expected := []string{
|
|
||||||
"AddItem",
|
|
||||||
"ChangeQuantity",
|
|
||||||
"RemoveItem",
|
|
||||||
"InitializeCheckout",
|
|
||||||
"OrderCreated",
|
|
||||||
"RemoveDelivery",
|
|
||||||
"SetDelivery",
|
|
||||||
"SetPickupPoint",
|
|
||||||
"ClearCartRequest",
|
|
||||||
"AddVoucher",
|
|
||||||
"RemoveVoucher",
|
|
||||||
"UpsertSubscriptionDetails",
|
|
||||||
"InventoryReserved",
|
|
||||||
"PreConditionFailed",
|
|
||||||
"SetUserId",
|
|
||||||
"LineItemMarking",
|
|
||||||
"RemoveLineItemMarking",
|
|
||||||
"SubscriptionAdded",
|
|
||||||
"PaymentDeclined",
|
|
||||||
"ConfirmationViewed",
|
|
||||||
"CreateCheckoutOrder",
|
|
||||||
"AddGiftcard",
|
|
||||||
"RemoveGiftcard",
|
|
||||||
}
|
|
||||||
|
|
||||||
names := reg.(*actor.ProtoMutationRegistry).RegisteredMutations()
|
|
||||||
for _, want := range expected {
|
|
||||||
if !slices.Contains(names, want) {
|
|
||||||
t.Fatalf("registry missing mutation %s; got %v", want, names)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create() by name returns correct concrete type.
|
|
||||||
for _, name := range expected {
|
|
||||||
msg, ok := reg.Create(name)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("Create failed for %s", name)
|
|
||||||
}
|
|
||||||
rt := reflect.TypeOf(msg)
|
|
||||||
if rt.Kind() == reflect.Ptr {
|
|
||||||
rt = rt.Elem()
|
|
||||||
}
|
|
||||||
if rt.Name() != name {
|
|
||||||
t.Fatalf("Create(%s) returned wrong type %s", name, rt.Name())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unregistered create
|
|
||||||
if m, ok := reg.Create("DoesNotExist"); ok || m != nil {
|
|
||||||
t.Fatalf("Create should fail for unknown; got (%T,%v)", m, ok)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTypeName sanity
|
|
||||||
add := &messages.AddItem{}
|
|
||||||
nm, ok := reg.GetTypeName(add)
|
|
||||||
if !ok || nm != "AddItem" {
|
|
||||||
t.Fatalf("GetTypeName failed for AddItem, got (%q,%v)", nm, ok)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply unregistered message -> should return error
|
|
||||||
results, err := reg.Apply(context.Background(), newTestGrain(), &messages.Noop{})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected error for unregistered mutation")
|
|
||||||
}
|
|
||||||
if len(results) != 1 || results[0].Error == nil || results[0].Error != actor.ErrMutationNotRegistered {
|
|
||||||
t.Fatalf("expected ApplyResult with ErrMutationNotRegistered, got %#v", results)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddItemAndMerging(t *testing.T) {
|
|
||||||
reg := newRegistry()
|
|
||||||
g := newTestGrain()
|
|
||||||
|
|
||||||
// Merge scenario (same SKU + same store pointer)
|
|
||||||
add1 := msgAddItem("SKU-1", 1000, 2, nil)
|
|
||||||
applyOK(t, reg, g, add1)
|
|
||||||
|
|
||||||
if len(g.Items) != 1 || g.Items[0].Quantity != 2 {
|
|
||||||
t.Fatalf("expected first item added; items=%d qty=%d", len(g.Items), g.Items[0].Quantity)
|
|
||||||
}
|
|
||||||
|
|
||||||
applyOK(t, reg, g, msgAddItem("SKU-1", 1000, 3, nil)) // should merge
|
|
||||||
if len(g.Items) != 1 || g.Items[0].Quantity != 5 {
|
|
||||||
t.Fatalf("expected merge quantity=5 items=%d qty=%d", len(g.Items), g.Items[0].Quantity)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Different store pointer -> new line
|
|
||||||
store := "S1"
|
|
||||||
applyOK(t, reg, g, msgAddItem("SKU-1", 1000, 1, &store))
|
|
||||||
if len(g.Items) != 2 {
|
|
||||||
t.Fatalf("expected second line for different store pointer; items=%d", len(g.Items))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Same store pointer & SKU -> merge with second line
|
|
||||||
applyOK(t, reg, g, msgAddItem("SKU-1", 1000, 4, &store))
|
|
||||||
if len(g.Items) != 2 || g.Items[1].Quantity != 5 {
|
|
||||||
t.Fatalf("expected merge on second line; items=%d second.qty=%d", len(g.Items), g.Items[1].Quantity)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalid quantity
|
|
||||||
applyErrorContains(t, reg, g, msgAddItem("BAD", 1000, 0, nil), "invalid quantity")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestChangeQuantityBehavior(t *testing.T) {
|
|
||||||
reg := newRegistry()
|
|
||||||
g := newTestGrain()
|
|
||||||
|
|
||||||
applyOK(t, reg, g, msgAddItem("A", 1500, 2, nil))
|
|
||||||
id := g.Items[0].Id
|
|
||||||
|
|
||||||
// Increase quantity
|
|
||||||
applyOK(t, reg, g, msgChangeQty(id, 5))
|
|
||||||
if g.Items[0].Quantity != 5 {
|
|
||||||
t.Fatalf("quantity not updated expected=5 got=%d", g.Items[0].Quantity)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove item by setting <=0
|
|
||||||
applyOK(t, reg, g, msgChangeQty(id, 0))
|
|
||||||
if len(g.Items) != 0 {
|
|
||||||
t.Fatalf("expected item removed; items=%d", len(g.Items))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not found
|
|
||||||
applyErrorContains(t, reg, g, msgChangeQty(9999, 1), "not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemoveItemBehavior(t *testing.T) {
|
|
||||||
reg := newRegistry()
|
|
||||||
g := newTestGrain()
|
|
||||||
|
|
||||||
applyOK(t, reg, g, msgAddItem("X", 1200, 1, nil))
|
|
||||||
id := g.Items[0].Id
|
|
||||||
|
|
||||||
applyOK(t, reg, g, msgRemoveItem(id))
|
|
||||||
if len(g.Items) != 0 {
|
|
||||||
t.Fatalf("expected item removed; items=%d", len(g.Items))
|
|
||||||
}
|
|
||||||
|
|
||||||
applyErrorContains(t, reg, g, msgRemoveItem(id), "not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeliveryMutations(t *testing.T) {
|
|
||||||
reg := newRegistry()
|
|
||||||
g := newTestGrain()
|
|
||||||
|
|
||||||
applyOK(t, reg, g, msgAddItem("D1", 1000, 1, nil))
|
|
||||||
applyOK(t, reg, g, msgAddItem("D2", 2000, 1, nil))
|
|
||||||
i1 := g.Items[0].Id
|
|
||||||
|
|
||||||
// Explicit items
|
|
||||||
applyOK(t, reg, g, msgSetDelivery("POSTNORD", i1))
|
|
||||||
if len(g.Deliveries) != 1 || len(g.Deliveries[0].Items) != 1 || g.Deliveries[0].Items[0] != i1 {
|
|
||||||
t.Fatalf("delivery not created as expected: %+v", g.Deliveries)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to attach an already-delivered item
|
|
||||||
applyErrorContains(t, reg, g, msgSetDelivery("POSTNORD", i1), "already has a delivery")
|
|
||||||
|
|
||||||
// Attach remaining item via empty list (auto include items without delivery)
|
|
||||||
applyOK(t, reg, g, msgSetDelivery("DHL"))
|
|
||||||
if len(g.Deliveries) != 2 {
|
|
||||||
t.Fatalf("expected second delivery; deliveries=%d", len(g.Deliveries))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-existent item
|
|
||||||
applyErrorContains(t, reg, g, msgSetDelivery("UPS", 99999), "not found")
|
|
||||||
|
|
||||||
// No eligible items left
|
|
||||||
applyErrorContains(t, reg, g, msgSetDelivery("UPS"), "no eligible items")
|
|
||||||
|
|
||||||
// Set pickup point on first delivery
|
|
||||||
did := g.Deliveries[0].Id
|
|
||||||
applyOK(t, reg, g, msgSetPickupPoint(did, "PP1"))
|
|
||||||
if g.Deliveries[0].PickupPoint == nil || g.Deliveries[0].PickupPoint.Id != "PP1" {
|
|
||||||
t.Fatalf("pickup point not set correctly: %+v", g.Deliveries[0].PickupPoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bad delivery id
|
|
||||||
applyErrorContains(t, reg, g, msgSetPickupPoint(9999, "PPX"), "delivery id")
|
|
||||||
|
|
||||||
// Remove delivery
|
|
||||||
applyOK(t, reg, g, &messages.RemoveDelivery{Id: did})
|
|
||||||
if len(g.Deliveries) != 1 || g.Deliveries[0].Id == did {
|
|
||||||
t.Fatalf("expected first delivery removed, remaining: %+v", g.Deliveries)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove delivery not found
|
|
||||||
applyErrorContains(t, reg, g, &messages.RemoveDelivery{Id: did}, "not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClearCart(t *testing.T) {
|
|
||||||
reg := newRegistry()
|
|
||||||
g := newTestGrain()
|
|
||||||
|
|
||||||
applyOK(t, reg, g, msgAddItem("X", 1000, 2, nil))
|
|
||||||
applyOK(t, reg, g, msgSetDelivery("P", g.Items[0].Id))
|
|
||||||
|
|
||||||
applyOK(t, reg, g, msgClearCart())
|
|
||||||
|
|
||||||
if len(g.Items) != 0 || len(g.Deliveries) != 0 {
|
|
||||||
t.Fatalf("expected cart cleared; items=%d deliveries=%d", len(g.Items), len(g.Deliveries))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVoucherMutations(t *testing.T) {
|
|
||||||
reg := newRegistry()
|
|
||||||
g := newTestGrain()
|
|
||||||
|
|
||||||
applyOK(t, reg, g, msgAddItem("VOUCH", 10000, 1, nil))
|
|
||||||
applyOK(t, reg, g, msgAddVoucher("PROMO", 5000))
|
|
||||||
|
|
||||||
if len(g.Vouchers) != 1 {
|
|
||||||
t.Fatalf("voucher not stored")
|
|
||||||
}
|
|
||||||
if g.TotalDiscount.IncVat != 5000 {
|
|
||||||
t.Fatalf("expected discount 5000 got %d", g.TotalDiscount.IncVat)
|
|
||||||
}
|
|
||||||
if g.TotalPrice.IncVat != 5000 {
|
|
||||||
t.Fatalf("expected total price 5000 got %d", g.TotalPrice.IncVat)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Duplicate voucher code
|
|
||||||
applyErrorContains(t, reg, g, msgAddVoucher("PROMO", 1000), "already applied")
|
|
||||||
|
|
||||||
// Add a large voucher (should not apply because value > total price)
|
|
||||||
applyOK(t, reg, g, msgAddVoucher("BIG", 100000))
|
|
||||||
if len(g.Vouchers) != 2 {
|
|
||||||
t.Fatalf("expected second voucher stored")
|
|
||||||
}
|
|
||||||
if g.TotalDiscount.IncVat != 5000 || g.TotalPrice.IncVat != 5000 {
|
|
||||||
t.Fatalf("large voucher incorrectly applied discount=%d total=%d",
|
|
||||||
g.TotalDiscount.IncVat, g.TotalPrice.IncVat)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove existing voucher
|
|
||||||
firstId := g.Vouchers[0].Id
|
|
||||||
applyOK(t, reg, g, msgRemoveVoucher(firstId))
|
|
||||||
|
|
||||||
if slices.ContainsFunc(g.Vouchers, func(v *Voucher) bool { return v.Id == firstId }) {
|
|
||||||
t.Fatalf("voucher id %d not removed", firstId)
|
|
||||||
}
|
|
||||||
// After removing PROMO, BIG remains but is not applied (exceeds price)
|
|
||||||
if g.TotalDiscount.IncVat != 0 || g.TotalPrice.IncVat != 10000 {
|
|
||||||
t.Fatalf("totals incorrect after removal discount=%d total=%d",
|
|
||||||
g.TotalDiscount.IncVat, g.TotalPrice.IncVat)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove not applied
|
|
||||||
applyErrorContains(t, reg, g, msgRemoveVoucher(firstId), "not applied")
|
|
||||||
}
|
|
||||||
|
|
||||||
// func TestCheckoutMutations(t *testing.T) {
|
|
||||||
// reg := newRegistry()
|
|
||||||
// g := newTestGrain()
|
|
||||||
|
|
||||||
// applyOK(t, reg, g, msgInitializeCheckout("ORD-1", "PENDING", true))
|
|
||||||
// if g.OrderReference != "ORD-1" || g.PaymentStatus != "PENDING" || !g.PaymentInProgress {
|
|
||||||
// t.Fatalf("initialize checkout failed: ref=%s status=%s inProgress=%v",
|
|
||||||
// g.OrderReference, g.PaymentStatus, g.PaymentInProgress)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// applyOK(t, reg, g, msgOrderCreated("ORD-1", "COMPLETED"))
|
|
||||||
// if g.OrderReference != "ORD-1" || g.PaymentStatus != "COMPLETED" || g.PaymentInProgress {
|
|
||||||
// t.Fatalf("order created mutation failed: ref=%s status=%s inProgress=%v",
|
|
||||||
// g.OrderReference, g.PaymentStatus, g.PaymentInProgress)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// applyErrorContains(t, reg, g, msgInitializeCheckout("", "X", true), "missing orderId")
|
|
||||||
// applyErrorContains(t, reg, g, msgOrderCreated("", "X"), "missing orderId")
|
|
||||||
// }
|
|
||||||
|
|
||||||
func TestSubscriptionDetailsMutation(t *testing.T) {
|
|
||||||
reg := newRegistry()
|
|
||||||
g := newTestGrain()
|
|
||||||
|
|
||||||
// Upsert new (Id == nil)
|
|
||||||
msgNew := &messages.UpsertSubscriptionDetails{
|
|
||||||
OfferingCode: "OFF1",
|
|
||||||
SigningType: "TYPE1",
|
|
||||||
}
|
|
||||||
applyOK(t, reg, g, msgNew)
|
|
||||||
if len(g.SubscriptionDetails) != 1 {
|
|
||||||
t.Fatalf("expected one subscription detail; got=%d", len(g.SubscriptionDetails))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capture created id
|
|
||||||
var createdId string
|
|
||||||
for k := range g.SubscriptionDetails {
|
|
||||||
createdId = k
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update existing
|
|
||||||
msgUpdate := &messages.UpsertSubscriptionDetails{
|
|
||||||
Id: &createdId,
|
|
||||||
OfferingCode: "OFF2",
|
|
||||||
SigningType: "TYPE2",
|
|
||||||
}
|
|
||||||
applyOK(t, reg, g, msgUpdate)
|
|
||||||
if g.SubscriptionDetails[createdId].OfferingCode != "OFF2" ||
|
|
||||||
g.SubscriptionDetails[createdId].SigningType != "TYPE2" {
|
|
||||||
t.Fatalf("subscription details not updated: %+v", g.SubscriptionDetails[createdId])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update non-existent
|
|
||||||
badId := "NON_EXISTENT"
|
|
||||||
applyErrorContains(t, reg, g, &messages.UpsertSubscriptionDetails{Id: &badId}, "not found")
|
|
||||||
|
|
||||||
// Nil mutation should be ignored and produce zero results.
|
|
||||||
resultsNil, errNil := reg.Apply(context.Background(), g, (*messages.UpsertSubscriptionDetails)(nil))
|
|
||||||
if errNil != nil {
|
|
||||||
t.Fatalf("unexpected error for nil mutation element: %v", errNil)
|
|
||||||
}
|
|
||||||
if len(resultsNil) != 0 {
|
|
||||||
t.Fatalf("expected zero results for nil mutation, got %d", len(resultsNil))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure registry Apply handles nil grain and nil message defensive errors consistently.
|
|
||||||
func TestRegistryDefensiveErrors(t *testing.T) {
|
|
||||||
reg := newRegistry()
|
|
||||||
g := newTestGrain()
|
|
||||||
|
|
||||||
// Nil grain
|
|
||||||
results, err := reg.Apply(context.Background(), nil, &messages.AddItem{})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected error for nil grain")
|
|
||||||
}
|
|
||||||
if len(results) != 0 {
|
|
||||||
t.Fatalf("expected no results for nil grain")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nil message slice
|
|
||||||
results, _ = reg.Apply(context.Background(), g, nil)
|
|
||||||
|
|
||||||
if len(results) != 0 {
|
|
||||||
t.Fatalf("expected no results when message slice nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubscriptionDetailsRequest struct {
|
|
||||||
Id *string `json:"id,omitempty"`
|
|
||||||
OfferingCode string `json:"offeringCode,omitempty"`
|
|
||||||
SigningType string `json:"signingType,omitempty"`
|
|
||||||
Data json.RawMessage `json:"data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sd *SubscriptionDetailsRequest) ToMessage() *messages.UpsertSubscriptionDetails {
|
|
||||||
return &messages.UpsertSubscriptionDetails{
|
|
||||||
Id: sd.Id,
|
|
||||||
OfferingCode: sd.OfferingCode,
|
|
||||||
SigningType: sd.SigningType,
|
|
||||||
Data: &anypb.Any{Value: sd.Data},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubscriptionDetailsJSONValidation(t *testing.T) {
|
|
||||||
reg := newRegistry()
|
|
||||||
g := newTestGrain()
|
|
||||||
|
|
||||||
// Valid JSON on create
|
|
||||||
jsonStr := `{"offeringCode": "OFFJSON", "signingType": "TYPEJSON", "data": {"value":"test","a":1}}`
|
|
||||||
var validCreate SubscriptionDetailsRequest
|
|
||||||
if err := json.Unmarshal([]byte(jsonStr), &validCreate); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
applyOK(t, reg, g, validCreate.ToMessage())
|
|
||||||
if len(g.SubscriptionDetails) != 1 {
|
|
||||||
t.Fatalf("expected one subscription detail after valid create, got %d", len(g.SubscriptionDetails))
|
|
||||||
}
|
|
||||||
var id string
|
|
||||||
for k := range g.SubscriptionDetails {
|
|
||||||
id = k
|
|
||||||
}
|
|
||||||
if string(g.SubscriptionDetails[id].Meta) != `{"value":"test","a":1}` {
|
|
||||||
t.Fatalf("expected meta stored as valid json, got %s", string(g.SubscriptionDetails[id].Meta))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update with valid JSON replaces meta
|
|
||||||
jsonStr2 := fmt.Sprintf(`{"id": "%s", "data": {"value": "eyJjaGFuZ2VkIjoxMjN9"}}`, id)
|
|
||||||
var updateValid messages.UpsertSubscriptionDetails
|
|
||||||
if err := json.Unmarshal([]byte(jsonStr2), &updateValid); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
applyOK(t, reg, g, &updateValid)
|
|
||||||
if string(g.SubscriptionDetails[id].Meta) != `{"changed":123}` {
|
|
||||||
t.Fatalf("expected meta updated to new json, got %s", string(g.SubscriptionDetails[id].Meta))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalid JSON on create
|
|
||||||
jsonStr3 := `{"offeringCode": "BAD", "signingType": "TYPE", "data": {"value": "eyJicm9rZW4iO30="}}`
|
|
||||||
var invalidCreate messages.UpsertSubscriptionDetails
|
|
||||||
if err := json.Unmarshal([]byte(jsonStr3), &invalidCreate); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
res := applyOne(t, reg, g, &invalidCreate)
|
|
||||||
if res.Error == nil || !strings.Contains(res.Error.Error(), "invalid json") {
|
|
||||||
t.Fatalf("expected invalid json error on create, got %v", res.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalid JSON on update
|
|
||||||
jsonStr4 := fmt.Sprintf(`{"id": "%s", "data": {"value": "e29vcHM="}}`, id)
|
|
||||||
var badUpdate messages.UpsertSubscriptionDetails
|
|
||||||
if err := json.Unmarshal([]byte(jsonStr4), &badUpdate); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
res2 := applyOne(t, reg, g, &badUpdate)
|
|
||||||
if res2.Error == nil || !strings.Contains(res2.Error.Error(), "invalid json") {
|
|
||||||
t.Fatalf("expected invalid json error on update, got %v", res2.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Empty Data should not overwrite existing meta
|
|
||||||
jsonStr5 := fmt.Sprintf(`{"id": "%s"}`, id)
|
|
||||||
var emptyUpdate messages.UpsertSubscriptionDetails
|
|
||||||
if err := json.Unmarshal([]byte(jsonStr5), &emptyUpdate); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
applyOK(t, reg, g, &emptyUpdate)
|
|
||||||
if string(g.SubscriptionDetails[id].Meta) != `{"changed":123}` {
|
|
||||||
t.Fatalf("empty update should not change meta, got %s", string(g.SubscriptionDetails[id].Meta))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSetUserId(t *testing.T) {
|
|
||||||
reg := newRegistry()
|
|
||||||
g := newTestGrain()
|
|
||||||
|
|
||||||
applyOK(t, reg, g, msgSetUserId("user123"))
|
|
||||||
if g.userId != "user123" {
|
|
||||||
t.Fatalf("expected userId=user123, got %s", g.userId)
|
|
||||||
}
|
|
||||||
|
|
||||||
applyErrorContains(t, reg, g, msgSetUserId(""), "cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLineItemMarking(t *testing.T) {
|
|
||||||
reg := newRegistry()
|
|
||||||
g := newTestGrain()
|
|
||||||
|
|
||||||
applyOK(t, reg, g, msgAddItem("MARK", 1000, 1, nil))
|
|
||||||
id := g.Items[0].Id
|
|
||||||
|
|
||||||
applyOK(t, reg, g, msgLineItemMarking(id, 1, "Gift message"))
|
|
||||||
if g.Items[0].Marking == nil || g.Items[0].Marking.Type != 1 || g.Items[0].Marking.Text != "Gift message" {
|
|
||||||
t.Fatalf("marking not set correctly: %+v", g.Items[0].Marking)
|
|
||||||
}
|
|
||||||
|
|
||||||
applyErrorContains(t, reg, g, msgLineItemMarking(9999, 2, "Test"), "not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemoveLineItemMarking(t *testing.T) {
|
|
||||||
reg := newRegistry()
|
|
||||||
g := newTestGrain()
|
|
||||||
|
|
||||||
applyOK(t, reg, g, msgAddItem("REMOVE", 1000, 1, nil))
|
|
||||||
id := g.Items[0].Id
|
|
||||||
|
|
||||||
// First set a marking
|
|
||||||
applyOK(t, reg, g, msgLineItemMarking(id, 1, "Test marking"))
|
|
||||||
if g.Items[0].Marking == nil || g.Items[0].Marking.Text != "Test marking" {
|
|
||||||
t.Fatalf("marking not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now remove it
|
|
||||||
applyOK(t, reg, g, msgRemoveLineItemMarking(id))
|
|
||||||
if g.Items[0].Marking != nil {
|
|
||||||
t.Fatalf("marking not removed")
|
|
||||||
}
|
|
||||||
|
|
||||||
applyErrorContains(t, reg, g, msgRemoveLineItemMarking(9999), "not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubscriptionAdded(t *testing.T) {
|
|
||||||
reg := newRegistry()
|
|
||||||
g := newTestGrain()
|
|
||||||
|
|
||||||
applyOK(t, reg, g, msgAddItem("SUB", 1000, 1, nil))
|
|
||||||
id := g.Items[0].Id
|
|
||||||
|
|
||||||
applyOK(t, reg, g, msgSubscriptionAdded(id, "det123", "ord456"))
|
|
||||||
if g.Items[0].SubscriptionDetailsId != "det123" || g.Items[0].OrderReference != "ord456" || !g.Items[0].IsSubscribed {
|
|
||||||
t.Fatalf("subscription not added: detailsId=%s orderRef=%s isSubscribed=%v",
|
|
||||||
g.Items[0].SubscriptionDetailsId, g.Items[0].OrderReference, g.Items[0].IsSubscribed)
|
|
||||||
}
|
|
||||||
|
|
||||||
applyErrorContains(t, reg, g, msgSubscriptionAdded(9999, "", ""), "not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// func TestPaymentDeclined(t *testing.T) {
|
|
||||||
// reg := newRegistry()
|
|
||||||
// g := newTestGrain()
|
|
||||||
|
|
||||||
// applyOK(t, reg, g, msgPaymentDeclined("Payment failed due to insufficient funds", "INSUFFICIENT_FUNDS"))
|
|
||||||
|
|
||||||
// if g.PaymentStatus != "declined" || g.CheckoutOrderId != "" {
|
|
||||||
// t.Fatalf("payment declined not handled: status=%s checkoutId=%s", g.PaymentStatus, g.CheckoutOrderId)
|
|
||||||
// }
|
|
||||||
// if len(g.PaymentDeclinedNotices) != 1 {
|
|
||||||
// t.Fatalf("expected 1 notice, got %d", len(g.PaymentDeclinedNotices))
|
|
||||||
// }
|
|
||||||
// notice := g.PaymentDeclinedNotices[0]
|
|
||||||
// if notice.Message != "Payment failed due to insufficient funds" {
|
|
||||||
// t.Fatalf("notice message not set correctly: %s", notice.Message)
|
|
||||||
// }
|
|
||||||
// if notice.Code == nil || *notice.Code != "INSUFFICIENT_FUNDS" {
|
|
||||||
// t.Fatalf("notice code not set correctly: %v", notice.Code)
|
|
||||||
// }
|
|
||||||
// if notice.Timestamp.IsZero() {
|
|
||||||
// t.Fatalf("notice timestamp not set")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
func TestConfirmationViewed(t *testing.T) {
|
|
||||||
reg := newRegistry()
|
|
||||||
g := newTestGrain()
|
|
||||||
|
|
||||||
// Initial state
|
|
||||||
if g.Confirmation != nil {
|
|
||||||
t.Fatalf("confirmation should be nil, got %v", g.Confirmation)
|
|
||||||
}
|
|
||||||
|
|
||||||
// First view
|
|
||||||
applyOK(t, reg, g, msgConfirmationViewed())
|
|
||||||
if g.Confirmation.ViewCount != 1 {
|
|
||||||
t.Fatalf("view count should be 1, got %d", g.Confirmation.ViewCount)
|
|
||||||
}
|
|
||||||
if g.Confirmation.LastViewedAt.IsZero() {
|
|
||||||
t.Fatalf("ConfirmationLastViewedAt not set")
|
|
||||||
}
|
|
||||||
firstTime := g.Confirmation.LastViewedAt
|
|
||||||
|
|
||||||
// Second view
|
|
||||||
applyOK(t, reg, g, msgConfirmationViewed())
|
|
||||||
if g.Confirmation.ViewCount != 2 {
|
|
||||||
t.Fatalf("view count should be 2, got %d", g.Confirmation.ViewCount)
|
|
||||||
}
|
|
||||||
if g.Confirmation.LastViewedAt == firstTime {
|
|
||||||
t.Fatalf("ConfirmationLastViewedAt should have updated")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateCheckoutOrder(t *testing.T) {
|
|
||||||
reg := newRegistry()
|
|
||||||
g := newTestGrain()
|
|
||||||
|
|
||||||
applyOK(t, reg, g, msgAddItem("CHECKOUT", 1000, 1, nil))
|
|
||||||
|
|
||||||
applyOK(t, reg, g, msgCreateCheckoutOrder("accepted", "SE"))
|
|
||||||
// if g.CheckoutOrderId == "" || g.CheckoutStatus != "pending" || g.CheckoutCountry != "SE" {
|
|
||||||
// t.Fatalf("checkout order not created: id=%s status=%s country=%s",
|
|
||||||
// g.CheckoutOrderId, g.CheckoutStatus, g.CheckoutCountry)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Empty cart
|
|
||||||
g2 := newTestGrain()
|
|
||||||
applyErrorContains(t, reg, g2, msgCreateCheckoutOrder("accepted", ""), "empty cart")
|
|
||||||
|
|
||||||
// Terms not accepted
|
|
||||||
applyErrorContains(t, reg, g, msgCreateCheckoutOrder("no", ""), "terms must be accepted")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddGiftcard(t *testing.T) {
|
|
||||||
reg := newRegistry()
|
|
||||||
g := newTestGrain()
|
|
||||||
|
|
||||||
designConfig, _ := anypb.New(&messages.AddItem{}) // example
|
|
||||||
applyOK(t, reg, g, msgAddGiftcard(5000, "2023-12-25", "John", "email", "Happy Birthday!", designConfig))
|
|
||||||
|
|
||||||
if len(g.Giftcards) != 1 {
|
|
||||||
t.Fatalf("expected 1 giftcard, got %d", len(g.Giftcards))
|
|
||||||
}
|
|
||||||
gc := g.Giftcards[0]
|
|
||||||
if gc.Value.IncVat != 5000 || gc.DeliveryDate != "2023-12-25" || gc.Recipient != "John" || gc.RecipientType != "email" || gc.Message != "Happy Birthday!" {
|
|
||||||
t.Fatalf("giftcard not set correctly: %+v", gc)
|
|
||||||
}
|
|
||||||
if g.TotalPrice.IncVat != 5000 {
|
|
||||||
t.Fatalf("total price not updated, got %d", g.TotalPrice.IncVat)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test invalid value
|
|
||||||
applyErrorContains(t, reg, g, msgAddGiftcard(0, "", "", "", "", nil), "must be positive")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemoveGiftcard(t *testing.T) {
|
|
||||||
reg := newRegistry()
|
|
||||||
g := newTestGrain()
|
|
||||||
|
|
||||||
applyOK(t, reg, g, msgAddGiftcard(1000, "2023-01-01", "Jane", "sms", "Cheers!", nil))
|
|
||||||
id := g.Giftcards[0].Id
|
|
||||||
|
|
||||||
applyOK(t, reg, g, msgRemoveGiftcard(id))
|
|
||||||
if len(g.Giftcards) != 0 {
|
|
||||||
t.Fatalf("giftcard not removed")
|
|
||||||
}
|
|
||||||
if g.TotalPrice.IncVat != 0 {
|
|
||||||
t.Fatalf("total price not updated after removal, got %d", g.TotalPrice.IncVat)
|
|
||||||
}
|
|
||||||
|
|
||||||
applyErrorContains(t, reg, g, msgRemoveGiftcard(id), "not found")
|
|
||||||
}
|
|
||||||
@@ -4,16 +4,14 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||||
)
|
)
|
||||||
|
|
||||||
func UpsertSubscriptionDetails(g *CartGrain, m *messages.UpsertSubscriptionDetails) error {
|
func UpsertSubscriptionDetails(g *CartGrain, m *messages.UpsertSubscriptionDetails) error {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if g.PaymentInProgress > 0 {
|
|
||||||
return ErrPaymentInProgress
|
|
||||||
}
|
|
||||||
metaBytes := m.Data.GetValue()
|
metaBytes := m.Data.GetValue()
|
||||||
|
|
||||||
// Create new subscription details when Id is nil.
|
// Create new subscription details when Id is nil.
|
||||||
|
|||||||
184
pkg/checkout/checkout-grain.go
Normal file
184
pkg/checkout/checkout-grain.go
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
package checkout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckoutId is the same as CartId for simplicity
|
||||||
|
type CheckoutId = cart.CartId
|
||||||
|
|
||||||
|
type PickupPoint struct {
|
||||||
|
DeliveryId uint32 `json:"deliveryId,omitempty"`
|
||||||
|
Id string `json:"id"`
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Address *string `json:"address,omitempty"`
|
||||||
|
City *string `json:"city,omitempty"`
|
||||||
|
Zip *string `json:"zip,omitempty"`
|
||||||
|
Country *string `json:"country,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckoutDelivery struct {
|
||||||
|
Id uint32 `json:"id"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Price cart.Price `json:"price"`
|
||||||
|
Items []uint32 `json:"items"`
|
||||||
|
PickupPoint *PickupPoint `json:"pickupPoint,omitempty"`
|
||||||
|
}
|
||||||
|
type PaymentStatus string
|
||||||
|
type CheckoutPaymentStatus PaymentStatus
|
||||||
|
|
||||||
|
const (
|
||||||
|
PaymentStatusPending PaymentStatus = "pending"
|
||||||
|
PaymentStatusFailed PaymentStatus = "failed"
|
||||||
|
PaymentStatusSuccess PaymentStatus = "success"
|
||||||
|
CheckoutPaymentStatusPending CheckoutPaymentStatus = "pending"
|
||||||
|
CheckoutPaymentStatusFailed CheckoutPaymentStatus = "failed"
|
||||||
|
CheckoutPaymentStatusSuccess CheckoutPaymentStatus = "success"
|
||||||
|
CheckoutPaymentStatusCancelled CheckoutPaymentStatus = "partial"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Payment struct {
|
||||||
|
PaymentId string `json:"paymentId"`
|
||||||
|
Status PaymentStatus `json:"status"`
|
||||||
|
Amount int64 `json:"amount"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
Provider string `json:"provider,omitempty"`
|
||||||
|
Method *string `json:"method,omitempty"`
|
||||||
|
Events []*PaymentEvent `json:"events,omitempty"`
|
||||||
|
ProcessorReference *string `json:"processorReference,omitempty"`
|
||||||
|
StartedAt *time.Time `json:"startedAt,omitempty"`
|
||||||
|
CompletedAt *time.Time `json:"completedAt,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaymentEvent struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payment) IsSettled() bool {
|
||||||
|
if p == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch p.Status {
|
||||||
|
case PaymentStatusSuccess:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfirmationStatus struct {
|
||||||
|
Code *string `json:"code,omitempty"`
|
||||||
|
ViewCount int `json:"viewCount"`
|
||||||
|
LastViewedAt time.Time `json:"lastViewedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckoutGrain struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
lastDeliveryId uint32
|
||||||
|
lastGiftcardId uint32
|
||||||
|
lastAccess time.Time
|
||||||
|
lastChange time.Time
|
||||||
|
Version uint32
|
||||||
|
Id CheckoutId `json:"id"`
|
||||||
|
CartId cart.CartId `json:"cartId"`
|
||||||
|
CartVersion uint64 `json:"cartVersion"`
|
||||||
|
CartState *cart.CartGrain `json:"cartState"` // snapshot of items
|
||||||
|
CartTotalPrice *cart.Price `json:"cartTotalPrice"`
|
||||||
|
OrderId *string `json:"orderId"`
|
||||||
|
Deliveries []*CheckoutDelivery `json:"deliveries,omitempty"`
|
||||||
|
PaymentInProgress uint16 `json:"paymentInProgress"`
|
||||||
|
InventoryReserved bool `json:"inventoryReserved"`
|
||||||
|
Confirmation *ConfirmationStatus `json:"confirmationViewed,omitempty"`
|
||||||
|
Payments []*Payment `json:"payments,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCheckoutGrain(id uint64, cartId cart.CartId, cartVersion uint64, ts time.Time, cartState *cart.CartGrain) *CheckoutGrain {
|
||||||
|
return &CheckoutGrain{
|
||||||
|
lastDeliveryId: 0,
|
||||||
|
lastGiftcardId: 0,
|
||||||
|
lastAccess: ts,
|
||||||
|
lastChange: ts,
|
||||||
|
Id: CheckoutId(id),
|
||||||
|
CartId: cartId,
|
||||||
|
CartVersion: cartVersion,
|
||||||
|
Deliveries: []*CheckoutDelivery{},
|
||||||
|
Payments: []*Payment{},
|
||||||
|
CartState: cartState,
|
||||||
|
CartTotalPrice: cartState.TotalPrice,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CheckoutGrain) GetId() uint64 {
|
||||||
|
return uint64(c.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CheckoutGrain) GetLastChange() time.Time {
|
||||||
|
return c.lastChange
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CheckoutGrain) GetLastAccess() time.Time {
|
||||||
|
return c.lastAccess
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CheckoutGrain) GetCurrentState() (*CheckoutGrain, error) {
|
||||||
|
c.lastAccess = time.Now()
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CheckoutGrain) GetState() ([]byte, error) {
|
||||||
|
return json.Marshal(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CheckoutGrain) FindPayment(paymentId string) (*Payment, bool) {
|
||||||
|
if paymentId == "" {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
for _, payment := range c.Payments {
|
||||||
|
if payment != nil && payment.PaymentId == paymentId {
|
||||||
|
return payment, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CheckoutGrain) SettledPayments() []*Payment {
|
||||||
|
if len(c.Payments) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
settled := make([]*Payment, 0, len(c.Payments))
|
||||||
|
for _, payment := range c.Payments {
|
||||||
|
if payment != nil && payment.IsSettled() {
|
||||||
|
settled = append(settled, payment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(settled) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return settled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CheckoutGrain) OpenPayments() []*Payment {
|
||||||
|
if len(c.Payments) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
pending := make([]*Payment, 0, len(c.Payments))
|
||||||
|
for _, payment := range c.Payments {
|
||||||
|
if payment == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !payment.IsSettled() {
|
||||||
|
pending = append(pending, payment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(pending) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return pending
|
||||||
|
}
|
||||||
31
pkg/checkout/mutation-context.go
Normal file
31
pkg/checkout/mutation-context.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package checkout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CheckoutMutationContext struct {
|
||||||
|
// Add any services needed, e.g., for delivery calculations, but since inventory is pre-handled, maybe none
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCheckoutMutationContext() *CheckoutMutationContext {
|
||||||
|
return &CheckoutMutationContext{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCheckoutMutationRegistry(ctx *CheckoutMutationContext) actor.MutationRegistry {
|
||||||
|
reg := actor.NewMutationRegistry()
|
||||||
|
reg.RegisterMutations(
|
||||||
|
actor.NewMutation(HandleInitializeCheckout),
|
||||||
|
actor.NewMutation(HandlePaymentStarted),
|
||||||
|
actor.NewMutation(HandlePaymentCompleted),
|
||||||
|
actor.NewMutation(HandlePaymentDeclined),
|
||||||
|
actor.NewMutation(HandlePaymentEvent),
|
||||||
|
actor.NewMutation(HandleConfirmationViewed),
|
||||||
|
actor.NewMutation(HandleOrderCreated),
|
||||||
|
actor.NewMutation(HandleInventoryReserved),
|
||||||
|
actor.NewMutation(HandleSetDelivery),
|
||||||
|
actor.NewMutation(HandleSetPickupPoint),
|
||||||
|
actor.NewMutation(HandleRemoveDelivery),
|
||||||
|
)
|
||||||
|
return reg
|
||||||
|
}
|
||||||
26
pkg/checkout/mutation_confirmation_viewed.go
Normal file
26
pkg/checkout/mutation_confirmation_viewed.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package checkout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleConfirmationViewed(g *CheckoutGrain, m *messages.ConfirmationViewed) error {
|
||||||
|
if m == nil {
|
||||||
|
return fmt.Errorf("ConfirmationViewed: nil payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.Confirmation != nil {
|
||||||
|
g.Confirmation = &ConfirmationStatus{
|
||||||
|
ViewCount: 1,
|
||||||
|
LastViewedAt: time.Now(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
g.Confirmation.ViewCount++
|
||||||
|
g.Confirmation.LastViewedAt = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
55
pkg/checkout/mutation_delivery.go
Normal file
55
pkg/checkout/mutation_delivery.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package checkout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
|
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||||
|
)
|
||||||
|
|
||||||
|
func asPickupPoint(p *messages.PickupPoint, deliveryId uint32) *PickupPoint {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &PickupPoint{
|
||||||
|
Id: p.Id,
|
||||||
|
Name: p.Name,
|
||||||
|
Address: p.Address,
|
||||||
|
City: p.City,
|
||||||
|
Country: p.Country,
|
||||||
|
Zip: p.Zip,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleSetDelivery mutation
|
||||||
|
// HandleSetDelivery mutation
|
||||||
|
func HandleSetDelivery(g *CheckoutGrain, m *messages.SetDelivery) error {
|
||||||
|
if m == nil {
|
||||||
|
return fmt.Errorf("HandleSetDelivery: nil payload")
|
||||||
|
}
|
||||||
|
if m.Provider == "" {
|
||||||
|
return fmt.Errorf("HandleSetDelivery: missing provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if delivery already exists, update or add
|
||||||
|
for _, d := range g.Deliveries {
|
||||||
|
if d.Provider == m.Provider {
|
||||||
|
// Update existing
|
||||||
|
d.Items = m.Items
|
||||||
|
d.PickupPoint = asPickupPoint(m.PickupPoint, d.Id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new delivery
|
||||||
|
g.lastDeliveryId++
|
||||||
|
delivery := &CheckoutDelivery{
|
||||||
|
Id: g.lastDeliveryId,
|
||||||
|
Provider: m.Provider,
|
||||||
|
Items: m.Items,
|
||||||
|
PickupPoint: asPickupPoint(m.PickupPoint, g.lastDeliveryId),
|
||||||
|
Price: *cart.NewPrice(), // Price might need calculation, but for now zero
|
||||||
|
}
|
||||||
|
g.Deliveries = append(g.Deliveries, delivery)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
44
pkg/checkout/mutation_initialize_checkout.go
Normal file
44
pkg/checkout/mutation_initialize_checkout.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package checkout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mutation_initialize_checkout.go
|
||||||
|
//
|
||||||
|
// Registers the InitializeCheckout mutation.
|
||||||
|
// This mutation is invoked AFTER an external checkout session
|
||||||
|
// has been successfully created or updated. It persists the
|
||||||
|
// order reference / status and marks the checkout as having a payment in progress.
|
||||||
|
//
|
||||||
|
// Behavior:
|
||||||
|
// - Sets OrderId to the order ID.
|
||||||
|
// - Sets Status to the current status.
|
||||||
|
// - Sets PaymentInProgress flag.
|
||||||
|
// - Assumes inventory is already reserved.
|
||||||
|
//
|
||||||
|
// Validation:
|
||||||
|
// - Returns an error if payload is nil.
|
||||||
|
// - Returns an error if orderId is empty.
|
||||||
|
|
||||||
|
func HandleInitializeCheckout(g *CheckoutGrain, m *messages.InitializeCheckout) error {
|
||||||
|
if m == nil {
|
||||||
|
return fmt.Errorf("InitializeCheckout: nil payload")
|
||||||
|
}
|
||||||
|
if m.OrderId == "" {
|
||||||
|
return fmt.Errorf("InitializeCheckout: missing orderId")
|
||||||
|
}
|
||||||
|
if m.CartState != nil {
|
||||||
|
return fmt.Errorf("InitializeCheckout: checkout already initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal(m.CartState.Value, &g.CartState)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("InitializeCheckout: failed to unmarshal cart state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
16
pkg/checkout/mutation_inventory_reserved.go
Normal file
16
pkg/checkout/mutation_inventory_reserved.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package checkout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleInventoryReserved(g *CheckoutGrain, m *messages.InventoryReserved) error {
|
||||||
|
if m == nil {
|
||||||
|
return fmt.Errorf("HandleInventoryReserved: nil payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
g.InventoryReserved = m.Status == "success"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package cart
|
package checkout
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mutation_order_created.go
|
// mutation_order_created.go
|
||||||
@@ -11,21 +11,20 @@ import (
|
|||||||
// Registers the OrderCreated mutation.
|
// Registers the OrderCreated mutation.
|
||||||
//
|
//
|
||||||
// This mutation represents the completion (or state transition) of an order
|
// This mutation represents the completion (or state transition) of an order
|
||||||
// initiated earlier via InitializeCheckout / external Klarna processing.
|
// initiated earlier via InitializeCheckout / external processing.
|
||||||
// It finalizes (or updates) the cart's order metadata.
|
// It finalizes (or updates) the checkout's order metadata.
|
||||||
//
|
//
|
||||||
// Behavior:
|
// Behavior:
|
||||||
// - Validates payload non-nil and OrderId not empty.
|
// - Validates payload non-nil and OrderId not empty.
|
||||||
// - Sets (or overwrites) OrderReference with the provided OrderId.
|
// - Sets Status from payload.Status.
|
||||||
// - Sets PaymentStatus from payload.Status.
|
// - Sets OrderId if not already set.
|
||||||
// - Marks PaymentInProgress = false (checkout flow finished / acknowledged).
|
// - Does NOT adjust monetary totals.
|
||||||
// - Does NOT adjust monetary totals (no WithTotals()).
|
|
||||||
//
|
//
|
||||||
// Notes / Future Extensions:
|
// Notes / Future Extensions:
|
||||||
// - If multiple order completion events can arrive (e.g., retries / webhook
|
// - If multiple order completion events can arrive (e.g., retries / webhook
|
||||||
// replays), this handler is idempotent: it simply overwrites fields.
|
// replays), this handler is idempotent: it simply overwrites fields.
|
||||||
// - If you need to guard against conflicting order IDs, add a check:
|
// - If you need to guard against conflicting order IDs, add a check:
|
||||||
// if g.OrderReference != "" && g.OrderReference != m.OrderId { ... }
|
// if g.OrderId != "" && g.OrderId != m.OrderId { ... }
|
||||||
// - Add audit logging or metrics here if required.
|
// - Add audit logging or metrics here if required.
|
||||||
//
|
//
|
||||||
// Concurrency:
|
// Concurrency:
|
||||||
@@ -33,16 +32,18 @@ import (
|
|||||||
// per grain. If out-of-order events are possible, embed versioning or
|
// per grain. If out-of-order events are possible, embed versioning or
|
||||||
// timestamps in the mutation and compare before applying changes.
|
// timestamps in the mutation and compare before applying changes.
|
||||||
|
|
||||||
func OrderCreated(g *CartGrain, m *messages.OrderCreated) error {
|
func HandleOrderCreated(g *CheckoutGrain, m *messages.OrderCreated) error {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return fmt.Errorf("OrderCreated: nil payload")
|
return fmt.Errorf("HandleOrderCreated: nil payload")
|
||||||
}
|
}
|
||||||
if m.OrderId == "" {
|
if m.OrderId == "" {
|
||||||
return fmt.Errorf("OrderCreated: missing orderId")
|
return fmt.Errorf("OrderCreated: missing orderId")
|
||||||
}
|
}
|
||||||
|
if g.OrderId == nil {
|
||||||
|
g.OrderId = &m.OrderId
|
||||||
|
} else if *g.OrderId != m.OrderId {
|
||||||
|
return fmt.Errorf("OrderCreated: conflicting order ID")
|
||||||
|
}
|
||||||
|
|
||||||
g.OrderReference = m.OrderId
|
|
||||||
//g.PaymentStatus = m.Status
|
|
||||||
//g.PaymentInProgress = false
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
37
pkg/checkout/mutation_payment_completed.go
Normal file
37
pkg/checkout/mutation_payment_completed.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package checkout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PaymentCompleted registers the completion of a payment for a checkout.
|
||||||
|
func HandlePaymentCompleted(g *CheckoutGrain, m *messages.PaymentCompleted) error {
|
||||||
|
if m == nil {
|
||||||
|
return fmt.Errorf("PaymentCompleted: nil payload")
|
||||||
|
}
|
||||||
|
paymentId := m.PaymentId
|
||||||
|
payment, found := g.FindPayment(paymentId)
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("PaymentCompleted: payment not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
payment.ProcessorReference = m.ProcessorReference
|
||||||
|
payment.Status = PaymentStatusSuccess
|
||||||
|
payment.Amount = m.Amount
|
||||||
|
payment.Currency = m.Currency
|
||||||
|
payment.CompletedAt = &time.Time{}
|
||||||
|
if m.CompletedAt != nil {
|
||||||
|
*payment.CompletedAt = m.CompletedAt.AsTime()
|
||||||
|
} else {
|
||||||
|
*payment.CompletedAt = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update checkout status
|
||||||
|
g.PaymentInProgress--
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
28
pkg/checkout/mutation_payment_declined.go
Normal file
28
pkg/checkout/mutation_payment_declined.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package checkout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||||
|
)
|
||||||
|
|
||||||
|
func asPointer[T any](value T) *T {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrPaymentNotFound = errors.New("payment not found")
|
||||||
|
|
||||||
|
func HandlePaymentDeclined(g *CheckoutGrain, m *messages.PaymentDeclined) error {
|
||||||
|
|
||||||
|
payment, found := g.FindPayment(m.PaymentId)
|
||||||
|
if !found {
|
||||||
|
return ErrPaymentNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
payment.CompletedAt = asPointer(time.Now())
|
||||||
|
payment.Status = "failed"
|
||||||
|
g.PaymentInProgress--
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
20
pkg/checkout/mutation_payment_event.go
Normal file
20
pkg/checkout/mutation_payment_event.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package checkout
|
||||||
|
|
||||||
|
import (
|
||||||
|
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandlePaymentEvent(g *CheckoutGrain, m *messages.PaymentEvent) error {
|
||||||
|
|
||||||
|
payment, found := g.FindPayment(m.PaymentId)
|
||||||
|
if !found {
|
||||||
|
return ErrPaymentNotFound
|
||||||
|
}
|
||||||
|
metaBytes := m.Data.Value
|
||||||
|
payment.Events = append(payment.Events, &PaymentEvent{
|
||||||
|
Name: m.Name,
|
||||||
|
Success: m.Success,
|
||||||
|
Data: metaBytes,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,48 +1,48 @@
|
|||||||
package cart
|
package checkout
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PaymentStarted registers the beginning of a payment attempt for a cart.
|
// PaymentStarted registers the beginning of a payment attempt for a checkout.
|
||||||
// It either upserts the payment entry (based on paymentId) or creates a new one,
|
// It either upserts the payment entry (based on paymentId) or creates a new one,
|
||||||
// marks the cart as having an in-progress payment, and recalculates the PaidInFull flag.
|
// marks the checkout as having a payment in progress.
|
||||||
func PaymentStarted(grain *CartGrain, msg *messages.PaymentStarted) error {
|
func HandlePaymentStarted(g *CheckoutGrain, m *messages.PaymentStarted) error {
|
||||||
if msg == nil {
|
if m == nil {
|
||||||
return fmt.Errorf("PaymentStarted: nil payload")
|
return fmt.Errorf("PaymentStarted: nil payload")
|
||||||
}
|
}
|
||||||
paymentID := strings.TrimSpace(msg.PaymentId)
|
paymentID := strings.TrimSpace(m.PaymentId)
|
||||||
if paymentID == "" {
|
if paymentID == "" {
|
||||||
return fmt.Errorf("PaymentStarted: missing paymentId")
|
return fmt.Errorf("PaymentStarted: missing paymentId")
|
||||||
}
|
}
|
||||||
if msg.Amount < 0 {
|
if m.Amount < 0 {
|
||||||
return fmt.Errorf("PaymentStarted: amount cannot be negative")
|
return fmt.Errorf("PaymentStarted: amount cannot be negative")
|
||||||
}
|
}
|
||||||
|
|
||||||
currency := strings.TrimSpace(msg.Currency)
|
currency := strings.TrimSpace(m.Currency)
|
||||||
provider := strings.TrimSpace(msg.Provider)
|
provider := strings.TrimSpace(m.Provider)
|
||||||
method := copyOptionalString(msg.Method)
|
method := copyOptionalString(m.Method)
|
||||||
|
|
||||||
startedAt := time.Now().UTC()
|
startedAt := time.Now().UTC()
|
||||||
if msg.StartedAt != nil {
|
if m.StartedAt != nil {
|
||||||
startedAt = msg.StartedAt.AsTime()
|
startedAt = m.StartedAt.AsTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
payment, found := grain.FindPayment(paymentID)
|
payment, found := g.FindPayment(paymentID)
|
||||||
|
|
||||||
if found {
|
if found {
|
||||||
if payment.Status != PaymentStatusPending {
|
if payment.Status != "pending" {
|
||||||
return fmt.Errorf("PaymentStarted: payment already started")
|
return fmt.Errorf("PaymentStarted: payment already started")
|
||||||
}
|
}
|
||||||
if payment.PaymentId != paymentID {
|
if payment.PaymentId != paymentID {
|
||||||
payment.PaymentId = paymentID
|
payment.PaymentId = paymentID
|
||||||
}
|
}
|
||||||
payment.Status = PaymentStatusPending
|
payment.Status = "pending"
|
||||||
payment.Amount = msg.Amount
|
payment.Amount = m.Amount
|
||||||
if currency != "" {
|
if currency != "" {
|
||||||
payment.Currency = currency
|
payment.Currency = currency
|
||||||
}
|
}
|
||||||
@@ -56,10 +56,11 @@ func PaymentStarted(grain *CartGrain, msg *messages.PaymentStarted) error {
|
|||||||
payment.CompletedAt = nil
|
payment.CompletedAt = nil
|
||||||
payment.ProcessorReference = nil
|
payment.ProcessorReference = nil
|
||||||
} else {
|
} else {
|
||||||
grain.Payments = append(grain.Payments, &CartPayment{
|
g.PaymentInProgress++
|
||||||
|
g.Payments = append(g.Payments, &Payment{
|
||||||
PaymentId: paymentID,
|
PaymentId: paymentID,
|
||||||
Status: PaymentStatusPending,
|
Status: "pending",
|
||||||
Amount: msg.Amount,
|
Amount: m.Amount,
|
||||||
Currency: currency,
|
Currency: currency,
|
||||||
Provider: provider,
|
Provider: provider,
|
||||||
Method: method,
|
Method: method,
|
||||||
@@ -67,9 +68,6 @@ func PaymentStarted(grain *CartGrain, msg *messages.PaymentStarted) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
grain.PaymentInProgress++
|
|
||||||
grain.PaymentStatus = PaymentStatusPending
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
3
pkg/checkout/mutation_payments.go
Normal file
3
pkg/checkout/mutation_payments.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package checkout
|
||||||
|
|
||||||
|
// This file is now empty as mutations have been moved to separate files.
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package cart
|
package checkout
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mutation_remove_delivery.go
|
// mutation_remove_delivery.go
|
||||||
@@ -13,26 +13,13 @@ import (
|
|||||||
// Behavior:
|
// Behavior:
|
||||||
// - Removes the delivery entry whose Id == payload.Id.
|
// - Removes the delivery entry whose Id == payload.Id.
|
||||||
// - If not found, returns an error.
|
// - If not found, returns an error.
|
||||||
// - Cart totals are recalculated (WithTotals) after removal.
|
|
||||||
// - Items previously associated with that delivery simply become "without delivery";
|
// - Items previously associated with that delivery simply become "without delivery";
|
||||||
// subsequent delivery mutations can reassign them.
|
// subsequent delivery mutations can reassign them.
|
||||||
//
|
|
||||||
// Differences vs legacy:
|
|
||||||
// - Legacy logic decremented TotalPrice explicitly before recalculating.
|
|
||||||
// Here we rely solely on UpdateTotals() to recompute from remaining
|
|
||||||
// deliveries and items (simpler / single source of truth).
|
|
||||||
//
|
|
||||||
// Future considerations:
|
|
||||||
// - If delivery pricing logic changes (e.g., dynamic taxes per delivery),
|
|
||||||
// UpdateTotals() may need enhancement to incorporate delivery tax properly.
|
|
||||||
|
|
||||||
func RemoveDelivery(g *CartGrain, m *messages.RemoveDelivery) error {
|
func HandleRemoveDelivery(g *CheckoutGrain, m *messages.RemoveDelivery) error {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return fmt.Errorf("RemoveDelivery: nil payload")
|
return fmt.Errorf("RemoveDelivery: nil payload")
|
||||||
}
|
}
|
||||||
if g.PaymentInProgress > 0 {
|
|
||||||
return ErrPaymentInProgress
|
|
||||||
}
|
|
||||||
targetID := uint32(m.Id)
|
targetID := uint32(m.Id)
|
||||||
index := -1
|
index := -1
|
||||||
for i, d := range g.Deliveries {
|
for i, d := range g.Deliveries {
|
||||||
@@ -47,6 +34,5 @@ func RemoveDelivery(g *CartGrain, m *messages.RemoveDelivery) error {
|
|||||||
|
|
||||||
// Remove delivery (order not preserved beyond necessity)
|
// Remove delivery (order not preserved beyond necessity)
|
||||||
g.Deliveries = append(g.Deliveries[:index], g.Deliveries[index+1:]...)
|
g.Deliveries = append(g.Deliveries[:index], g.Deliveries[index+1:]...)
|
||||||
g.UpdateTotals()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package cart
|
package checkout
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
messages "git.k6n.net/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mutation_set_pickup_point.go
|
// mutation_set_pickup_point.go
|
||||||
@@ -29,37 +29,23 @@ import (
|
|||||||
// - Track history / audit of pickup point changes
|
// - Track history / audit of pickup point changes
|
||||||
// - Trigger delivery price adjustments (which would then require WithTotals()).
|
// - Trigger delivery price adjustments (which would then require WithTotals()).
|
||||||
|
|
||||||
func SetPickupPoint(g *CartGrain, m *messages.SetPickupPoint) error {
|
func HandleSetPickupPoint(g *CheckoutGrain, m *messages.SetPickupPoint) error {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return fmt.Errorf("SetPickupPoint: nil payload")
|
return fmt.Errorf("SetPickupPoint: nil payload")
|
||||||
}
|
}
|
||||||
if g.PaymentInProgress > 0 {
|
|
||||||
return ErrPaymentInProgress
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, d := range g.Deliveries {
|
for _, d := range g.Deliveries {
|
||||||
if d.Id == uint32(m.DeliveryId) {
|
if d.Id == uint32(m.DeliveryId) {
|
||||||
d.PickupPoint = &messages.PickupPoint{
|
d.PickupPoint = &PickupPoint{
|
||||||
Id: m.Id,
|
Id: m.PickupPoint.Id,
|
||||||
Name: m.Name,
|
Name: m.PickupPoint.Name,
|
||||||
Address: m.Address,
|
Address: m.PickupPoint.Address,
|
||||||
City: m.City,
|
City: m.PickupPoint.City,
|
||||||
Zip: m.Zip,
|
Zip: m.PickupPoint.Zip,
|
||||||
Country: m.Country,
|
Country: m.PickupPoint.Country,
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fmt.Errorf("SetPickupPoint: delivery id %d not found", m.DeliveryId)
|
return fmt.Errorf("SetPickupPoint: delivery id %d not found", m.DeliveryId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClearCart(g *CartGrain, m *messages.ClearCartRequest) error {
|
|
||||||
if m == nil {
|
|
||||||
return fmt.Errorf("ClearCart: nil payload")
|
|
||||||
}
|
|
||||||
// maybe check if payment is done?
|
|
||||||
g.Deliveries = g.Deliveries[:0]
|
|
||||||
g.Items = g.Items[:0]
|
|
||||||
g.UpdateTotals()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -16,15 +16,14 @@ import (
|
|||||||
type K8sDiscovery struct {
|
type K8sDiscovery struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
client *kubernetes.Clientset
|
client *kubernetes.Clientset
|
||||||
|
listOptions metav1.ListOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *K8sDiscovery) Discover() ([]string, error) {
|
func (k *K8sDiscovery) Discover() ([]string, error) {
|
||||||
return k.DiscoverInNamespace("")
|
return k.DiscoverInNamespace("")
|
||||||
}
|
}
|
||||||
func (k *K8sDiscovery) DiscoverInNamespace(namespace string) ([]string, error) {
|
func (k *K8sDiscovery) DiscoverInNamespace(namespace string) ([]string, error) {
|
||||||
pods, err := k.client.CoreV1().Pods(namespace).List(k.ctx, metav1.ListOptions{
|
pods, err := k.client.CoreV1().Pods(namespace).List(k.ctx, k.listOptions)
|
||||||
LabelSelector: "actor-pool=cart",
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -44,14 +43,10 @@ func hasReadyCondition(pod *v1.Pod) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (k *K8sDiscovery) Watch() (<-chan HostChange, error) {
|
func (k *K8sDiscovery) Watch() (<-chan HostChange, error) {
|
||||||
timeout := int64(30)
|
|
||||||
ipsThatAreReady := make(map[string]bool)
|
ipsThatAreReady := make(map[string]bool)
|
||||||
m := sync.Mutex{}
|
m := sync.Mutex{}
|
||||||
watcherFn := func(options metav1.ListOptions) (watch.Interface, error) {
|
watcherFn := func(options metav1.ListOptions) (watch.Interface, error) {
|
||||||
return k.client.CoreV1().Pods("").Watch(k.ctx, metav1.ListOptions{
|
return k.client.CoreV1().Pods("").Watch(k.ctx, k.listOptions)
|
||||||
LabelSelector: "actor-pool=cart",
|
|
||||||
TimeoutSeconds: &timeout,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
watcher, err := toolsWatch.NewRetryWatcherWithContext(k.ctx, "1", &cache.ListWatch{WatchFunc: watcherFn})
|
watcher, err := toolsWatch.NewRetryWatcherWithContext(k.ctx, "1", &cache.ListWatch{WatchFunc: watcherFn})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -82,9 +77,10 @@ func (k *K8sDiscovery) Watch() (<-chan HostChange, error) {
|
|||||||
return ch, nil
|
return ch, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewK8sDiscovery(client *kubernetes.Clientset) *K8sDiscovery {
|
func NewK8sDiscovery(client *kubernetes.Clientset, listOptions metav1.ListOptions) *K8sDiscovery {
|
||||||
return &K8sDiscovery{
|
return &K8sDiscovery{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
client: client,
|
client: client,
|
||||||
|
listOptions: listOptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/tools/clientcmd"
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
)
|
)
|
||||||
@@ -17,7 +18,9 @@ func TestDiscovery(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error creating client: %v", err)
|
t.Errorf("Error creating client: %v", err)
|
||||||
}
|
}
|
||||||
d := NewK8sDiscovery(client)
|
d := NewK8sDiscovery(client, metav1.ListOptions{
|
||||||
|
LabelSelector: "app",
|
||||||
|
})
|
||||||
res, err := d.DiscoverInNamespace("")
|
res, err := d.DiscoverInNamespace("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error discovering: %v", err)
|
t.Errorf("Error discovering: %v", err)
|
||||||
@@ -36,7 +39,9 @@ func TestWatch(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error creating client: %v", err)
|
t.Errorf("Error creating client: %v", err)
|
||||||
}
|
}
|
||||||
d := NewK8sDiscovery(client)
|
d := NewK8sDiscovery(client, metav1.ListOptions{
|
||||||
|
LabelSelector: "app",
|
||||||
|
})
|
||||||
ch, err := d.Watch()
|
ch, err := d.Watch()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error watching: %v", err)
|
t.Errorf("Error watching: %v", err)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.k6n.net/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/control"
|
||||||
"go.opentelemetry.io/contrib/bridges/otelslog"
|
"go.opentelemetry.io/contrib/bridges/otelslog"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.k6n.net/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Rule struct {
|
type Rule struct {
|
||||||
|
|||||||
96
proto/cart.proto
Normal file
96
proto/cart.proto
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
package cart_messages;
|
||||||
|
option go_package = "git.k6n.net/go-cart-actor/proto/cart;cart_messages";
|
||||||
|
|
||||||
|
import "google/protobuf/any.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
message ClearCartRequest {}
|
||||||
|
|
||||||
|
message AddItem {
|
||||||
|
uint32 item_id = 1;
|
||||||
|
int32 quantity = 2;
|
||||||
|
int64 price = 3;
|
||||||
|
int64 orgPrice = 9;
|
||||||
|
string sku = 4;
|
||||||
|
string name = 5;
|
||||||
|
string image = 6;
|
||||||
|
int32 stock = 7;
|
||||||
|
int32 tax = 8;
|
||||||
|
string brand = 13;
|
||||||
|
string category = 14;
|
||||||
|
string category2 = 15;
|
||||||
|
string category3 = 16;
|
||||||
|
string category4 = 17;
|
||||||
|
string category5 = 18;
|
||||||
|
string disclaimer = 10;
|
||||||
|
string articleType = 11;
|
||||||
|
string sellerId = 19;
|
||||||
|
string sellerName = 20;
|
||||||
|
string country = 21;
|
||||||
|
string saleStatus = 24;
|
||||||
|
optional string outlet = 12;
|
||||||
|
optional string storeId = 22;
|
||||||
|
optional uint32 parentId = 23;
|
||||||
|
string cgm = 25;
|
||||||
|
optional google.protobuf.Timestamp reservationEndTime = 26;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoveItem { uint32 Id = 1; }
|
||||||
|
|
||||||
|
message ChangeQuantity {
|
||||||
|
uint32 Id = 1;
|
||||||
|
int32 quantity = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetUserId {
|
||||||
|
string userId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LineItemMarking {
|
||||||
|
uint32 id = 1;
|
||||||
|
uint32 type = 2;
|
||||||
|
string marking = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoveLineItemMarking {
|
||||||
|
uint32 id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SubscriptionAdded {
|
||||||
|
uint32 itemId = 1;
|
||||||
|
string detailsId = 3;
|
||||||
|
string orderReference = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddVoucher {
|
||||||
|
string code = 1;
|
||||||
|
int64 value = 2;
|
||||||
|
repeated string voucherRules = 3;
|
||||||
|
string description = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoveVoucher { uint32 id = 1; }
|
||||||
|
|
||||||
|
message UpsertSubscriptionDetails {
|
||||||
|
optional string id = 1;
|
||||||
|
string offeringCode = 2;
|
||||||
|
string signingType = 3;
|
||||||
|
google.protobuf.Any data = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Mutation {
|
||||||
|
oneof type {
|
||||||
|
ClearCartRequest clear_cart = 1;
|
||||||
|
AddItem add_item = 2;
|
||||||
|
RemoveItem remove_item = 3;
|
||||||
|
ChangeQuantity change_quantity = 4;
|
||||||
|
SetUserId set_user_id = 5;
|
||||||
|
LineItemMarking line_item_marking = 6;
|
||||||
|
RemoveLineItemMarking remove_line_item_marking = 7;
|
||||||
|
SubscriptionAdded subscription_added = 8;
|
||||||
|
AddVoucher add_voucher = 20;
|
||||||
|
RemoveVoucher remove_voucher = 21;
|
||||||
|
UpsertSubscriptionDetails upsert_subscription_details = 22;
|
||||||
|
}
|
||||||
|
}
|
||||||
1266
proto/cart/cart.pb.go
Normal file
1266
proto/cart/cart.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
106
proto/checkout.proto
Normal file
106
proto/checkout.proto
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
package checkout_messages;
|
||||||
|
option go_package = "git.k6n.net/go-cart-actor/proto/checkout;checkout_messages";
|
||||||
|
|
||||||
|
import "google/protobuf/any.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
message SetDelivery {
|
||||||
|
string provider = 1;
|
||||||
|
repeated uint32 items = 2;
|
||||||
|
optional PickupPoint pickupPoint = 3;
|
||||||
|
string country = 4;
|
||||||
|
string zip = 5;
|
||||||
|
optional string address = 6;
|
||||||
|
optional string city = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetPickupPoint {
|
||||||
|
uint32 deliveryId = 1;
|
||||||
|
PickupPoint pickupPoint = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PickupPoint {
|
||||||
|
string id = 2;
|
||||||
|
optional string name = 3;
|
||||||
|
optional string address = 4;
|
||||||
|
optional string city = 5;
|
||||||
|
optional string zip = 6;
|
||||||
|
optional string country = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RemoveDelivery { uint32 id = 1; }
|
||||||
|
|
||||||
|
message PaymentStarted {
|
||||||
|
string paymentId = 1;
|
||||||
|
int64 amount = 3;
|
||||||
|
string currency = 4;
|
||||||
|
string provider = 5;
|
||||||
|
optional string method = 6;
|
||||||
|
optional google.protobuf.Timestamp startedAt = 7;
|
||||||
|
repeated uint32 items = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PaymentCompleted {
|
||||||
|
string paymentId = 1;
|
||||||
|
string status = 2;
|
||||||
|
int64 amount = 3;
|
||||||
|
string currency = 4;
|
||||||
|
optional string processorReference = 5;
|
||||||
|
optional google.protobuf.Timestamp completedAt = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PaymentDeclined {
|
||||||
|
string paymentId = 1;
|
||||||
|
string message = 2;
|
||||||
|
optional string code = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PaymentEvent {
|
||||||
|
string paymentId = 1;
|
||||||
|
string name = 2;
|
||||||
|
bool success = 3;
|
||||||
|
google.protobuf.Any data = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ConfirmationViewed {
|
||||||
|
google.protobuf.Timestamp viewedAt = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OrderCreated {
|
||||||
|
string orderId = 1;
|
||||||
|
string status = 2;
|
||||||
|
google.protobuf.Timestamp createdAt = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InitializeCheckout {
|
||||||
|
string orderId = 1;
|
||||||
|
uint64 cartId = 2;
|
||||||
|
uint32 version = 3;
|
||||||
|
google.protobuf.Any cartState = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InventoryReserved {
|
||||||
|
string id = 1;
|
||||||
|
string status = 2;
|
||||||
|
optional string message = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Mutation {
|
||||||
|
oneof type {
|
||||||
|
SetDelivery set_delivery = 1;
|
||||||
|
SetPickupPoint set_pickup_point = 2;
|
||||||
|
RemoveDelivery remove_delivery = 3;
|
||||||
|
PaymentDeclined payment_declined = 4;
|
||||||
|
ConfirmationViewed confirmation_viewed = 5;
|
||||||
|
|
||||||
|
OrderCreated order_created = 7;
|
||||||
|
|
||||||
|
InitializeCheckout initialize_checkout = 9;
|
||||||
|
InventoryReserved inventory_reserved = 10;
|
||||||
|
PaymentStarted payment_started = 11;
|
||||||
|
PaymentCompleted payment_completed = 12;
|
||||||
|
PaymentEvent payment_event = 13;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
1349
proto/checkout/checkout.pb.go
Normal file
1349
proto/checkout/checkout.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
|||||||
// protoc v6.33.1
|
// protoc v6.33.1
|
||||||
// source: control_plane.proto
|
// source: control_plane.proto
|
||||||
|
|
||||||
package messages
|
package control_plane_messages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
@@ -640,84 +640,100 @@ var File_control_plane_proto protoreflect.FileDescriptor
|
|||||||
|
|
||||||
var file_control_plane_proto_rawDesc = string([]byte{
|
var file_control_plane_proto_rawDesc = string([]byte{
|
||||||
0x0a, 0x13, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e,
|
0x0a, 0x13, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e,
|
||||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x1a,
|
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x16, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70,
|
||||||
0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
|
0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x1a, 0x19, 0x67,
|
||||||
0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d,
|
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61,
|
||||||
0x70, 0x74, 0x79, 0x22, 0x3c, 0x0a, 0x09, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x70, 0x6c, 0x79,
|
0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74,
|
||||||
0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
0x79, 0x22, 0x3c, 0x0a, 0x09, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x12,
|
||||||
0x68, 0x6f, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x75, 0x6e, 0x69, 0x78, 0x5f, 0x74, 0x69, 0x6d,
|
0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f,
|
||||||
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x75, 0x6e, 0x69, 0x78, 0x54, 0x69, 0x6d,
|
0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x75, 0x6e, 0x69, 0x78, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18,
|
||||||
0x65, 0x22, 0x33, 0x0a, 0x10, 0x4e, 0x65, 0x67, 0x6f, 0x74, 0x69, 0x61, 0x74, 0x65, 0x52, 0x65,
|
0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x75, 0x6e, 0x69, 0x78, 0x54, 0x69, 0x6d, 0x65, 0x22,
|
||||||
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x5f, 0x68,
|
0x33, 0x0a, 0x10, 0x4e, 0x65, 0x67, 0x6f, 0x74, 0x69, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75,
|
||||||
0x6f, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6b, 0x6e, 0x6f, 0x77,
|
0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x5f, 0x68, 0x6f, 0x73,
|
||||||
0x6e, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x22, 0x26, 0x0a, 0x0e, 0x4e, 0x65, 0x67, 0x6f, 0x74, 0x69,
|
0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x48,
|
||||||
0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x68, 0x6f, 0x73, 0x74,
|
0x6f, 0x73, 0x74, 0x73, 0x22, 0x26, 0x0a, 0x0e, 0x4e, 0x65, 0x67, 0x6f, 0x74, 0x69, 0x61, 0x74,
|
||||||
0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x22, 0x21,
|
0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x18,
|
||||||
0x0a, 0x0d, 0x41, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12,
|
0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x22, 0x21, 0x0a, 0x0d,
|
||||||
0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x04, 0x52, 0x03, 0x69, 0x64,
|
0x41, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x10, 0x0a,
|
||||||
0x73, 0x22, 0x46, 0x0a, 0x0e, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65,
|
0x03, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x04, 0x52, 0x03, 0x69, 0x64, 0x73, 0x22,
|
||||||
0x41, 0x63, 0x6b, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x18,
|
0x46, 0x0a, 0x0e, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x41, 0x63,
|
||||||
0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x12,
|
0x6b, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20,
|
||||||
0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a,
|
||||||
0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x23, 0x0a, 0x0d, 0x43, 0x6c, 0x6f,
|
0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
|
||||||
0x73, 0x69, 0x6e, 0x67, 0x4e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f,
|
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x23, 0x0a, 0x0d, 0x43, 0x6c, 0x6f, 0x73, 0x69,
|
||||||
0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x22, 0x39,
|
0x6e, 0x67, 0x4e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74,
|
||||||
0x0a, 0x11, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x41, 0x6e, 0x6e, 0x6f, 0x75,
|
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x22, 0x39, 0x0a, 0x11,
|
||||||
0x6e, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28,
|
0x4f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63,
|
||||||
0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x02,
|
0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
0x20, 0x03, 0x28, 0x04, 0x52, 0x03, 0x69, 0x64, 0x73, 0x22, 0x36, 0x0a, 0x0e, 0x45, 0x78, 0x70,
|
0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03,
|
||||||
0x69, 0x72, 0x79, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68,
|
0x28, 0x04, 0x52, 0x03, 0x69, 0x64, 0x73, 0x22, 0x36, 0x0a, 0x0e, 0x45, 0x78, 0x70, 0x69, 0x72,
|
||||||
0x6f, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x12,
|
0x79, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73,
|
||||||
0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x04, 0x52, 0x03, 0x69, 0x64,
|
0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x10, 0x0a,
|
||||||
0x73, 0x22, 0x50, 0x0a, 0x0c, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
0x03, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x04, 0x52, 0x03, 0x69, 0x64, 0x73, 0x22,
|
||||||
0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69,
|
0x50, 0x0a, 0x0c, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
||||||
0x64, 0x12, 0x30, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x02, 0x20,
|
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12,
|
||||||
0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
|
0x30, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
|
||||||
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61,
|
0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||||
0x67, 0x65, 0x73, 0x22, 0x1c, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
|
||||||
0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69,
|
0x73, 0x22, 0x1c, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
||||||
0x64, 0x22, 0x36, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x2a, 0x0a,
|
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x22,
|
||||||
0x05, 0x67, 0x72, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67,
|
0x36, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x67,
|
||||||
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41,
|
0x72, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f,
|
||||||
0x6e, 0x79, 0x52, 0x05, 0x67, 0x72, 0x61, 0x69, 0x6e, 0x22, 0x29, 0x0a, 0x0b, 0x41, 0x70, 0x70,
|
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79,
|
||||||
0x6c, 0x79, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x65,
|
0x52, 0x05, 0x67, 0x72, 0x61, 0x69, 0x6e, 0x22, 0x29, 0x0a, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x79,
|
||||||
0x70, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x63, 0x63, 0x65,
|
0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74,
|
||||||
0x70, 0x74, 0x65, 0x64, 0x32, 0xf6, 0x03, 0x0a, 0x0c, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c,
|
0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74,
|
||||||
0x50, 0x6c, 0x61, 0x6e, 0x65, 0x12, 0x2c, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x0f, 0x2e,
|
0x65, 0x64, 0x32, 0xd6, 0x05, 0x0a, 0x0c, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x50, 0x6c,
|
||||||
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x13,
|
0x61, 0x6e, 0x65, 0x12, 0x48, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x1d, 0x2e, 0x63, 0x6f,
|
||||||
0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65,
|
0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73,
|
||||||
0x70, 0x6c, 0x79, 0x12, 0x41, 0x0a, 0x09, 0x4e, 0x65, 0x67, 0x6f, 0x74, 0x69, 0x61, 0x74, 0x65,
|
0x61, 0x67, 0x65, 0x73, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x21, 0x2e, 0x63, 0x6f, 0x6e,
|
||||||
0x12, 0x1a, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x4e, 0x65, 0x67, 0x6f,
|
0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61,
|
||||||
0x74, 0x69, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x6d,
|
0x67, 0x65, 0x73, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x5d, 0x0a,
|
||||||
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x4e, 0x65, 0x67, 0x6f, 0x74, 0x69, 0x61, 0x74,
|
0x09, 0x4e, 0x65, 0x67, 0x6f, 0x74, 0x69, 0x61, 0x74, 0x65, 0x12, 0x28, 0x2e, 0x63, 0x6f, 0x6e,
|
||||||
0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x3c, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x63,
|
0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61,
|
||||||
0x61, 0x6c, 0x41, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x73, 0x12, 0x0f, 0x2e, 0x6d, 0x65, 0x73,
|
0x67, 0x65, 0x73, 0x2e, 0x4e, 0x65, 0x67, 0x6f, 0x74, 0x69, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71,
|
||||||
0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x17, 0x2e, 0x6d, 0x65,
|
0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70,
|
||||||
0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x41, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x73, 0x52,
|
0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x4e, 0x65,
|
||||||
0x65, 0x70, 0x6c, 0x79, 0x12, 0x4a, 0x0a, 0x11, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65,
|
0x67, 0x6f, 0x74, 0x69, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x58, 0x0a, 0x10,
|
||||||
0x4f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x12, 0x1b, 0x2e, 0x6d, 0x65, 0x73, 0x73,
|
0x47, 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x41, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x73,
|
||||||
|
0x12, 0x1d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65,
|
||||||
|
0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a,
|
||||||
|
0x25, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5f,
|
||||||
|
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x41, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64,
|
||||||
|
0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x66, 0x0a, 0x11, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e,
|
||||||
|
0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x12, 0x29, 0x2e, 0x63, 0x6f,
|
||||||
|
0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73,
|
||||||
0x61, 0x67, 0x65, 0x73, 0x2e, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x41, 0x6e,
|
0x61, 0x67, 0x65, 0x73, 0x2e, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x41, 0x6e,
|
||||||
0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x1a, 0x18, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
|
0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x1a, 0x26, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c,
|
||||||
0x73, 0x2e, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x41, 0x63, 0x6b,
|
0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e,
|
||||||
0x12, 0x36, 0x0a, 0x05, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x12, 0x16, 0x2e, 0x6d, 0x65, 0x73, 0x73,
|
0x4f, 0x77, 0x6e, 0x65, 0x72, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x41, 0x63, 0x6b, 0x12, 0x52,
|
||||||
0x61, 0x67, 0x65, 0x73, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
0x0a, 0x05, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x12, 0x24, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f,
|
||||||
0x74, 0x1a, 0x15, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x41, 0x70, 0x70,
|
0x6c, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73,
|
||||||
0x6c, 0x79, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x44, 0x0a, 0x0e, 0x41, 0x6e, 0x6e, 0x6f,
|
0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e,
|
||||||
0x75, 0x6e, 0x63, 0x65, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x18, 0x2e, 0x6d, 0x65, 0x73,
|
0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x6d, 0x65,
|
||||||
0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x41, 0x6e, 0x6e, 0x6f,
|
0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x73, 0x75,
|
||||||
0x75, 0x6e, 0x63, 0x65, 0x1a, 0x18, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e,
|
0x6c, 0x74, 0x12, 0x60, 0x0a, 0x0e, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x45, 0x78,
|
||||||
0x4f, 0x77, 0x6e, 0x65, 0x72, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x41, 0x63, 0x6b, 0x12, 0x3c,
|
0x70, 0x69, 0x72, 0x79, 0x12, 0x26, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70,
|
||||||
0x0a, 0x07, 0x43, 0x6c, 0x6f, 0x73, 0x69, 0x6e, 0x67, 0x12, 0x17, 0x2e, 0x6d, 0x65, 0x73, 0x73,
|
0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x45, 0x78,
|
||||||
0x61, 0x67, 0x65, 0x73, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x69, 0x6e, 0x67, 0x4e, 0x6f, 0x74, 0x69,
|
0x70, 0x69, 0x72, 0x79, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x1a, 0x26, 0x2e, 0x63,
|
||||||
0x63, 0x65, 0x1a, 0x18, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x4f, 0x77,
|
0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x6d, 0x65, 0x73,
|
||||||
0x6e, 0x65, 0x72, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x41, 0x63, 0x6b, 0x12, 0x2f, 0x0a, 0x03,
|
0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x43, 0x68, 0x61, 0x6e, 0x67,
|
||||||
0x47, 0x65, 0x74, 0x12, 0x14, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x47,
|
0x65, 0x41, 0x63, 0x6b, 0x12, 0x58, 0x0a, 0x07, 0x43, 0x6c, 0x6f, 0x73, 0x69, 0x6e, 0x67, 0x12,
|
||||||
0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x6d, 0x65, 0x73, 0x73,
|
0x25, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5f,
|
||||||
0x61, 0x67, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x42, 0x2a, 0x5a,
|
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x69, 0x6e, 0x67,
|
||||||
0x28, 0x67, 0x69, 0x74, 0x2e, 0x6b, 0x36, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2f, 0x67, 0x6f, 0x2d,
|
0x4e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x1a, 0x26, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c,
|
||||||
0x63, 0x61, 0x72, 0x74, 0x2d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e,
|
||||||
0x3b, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
0x4f, 0x77, 0x6e, 0x65, 0x72, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x41, 0x63, 0x6b, 0x12, 0x4b,
|
||||||
0x33,
|
0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x22, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f,
|
||||||
|
0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x47,
|
||||||
|
0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x63, 0x6f, 0x6e, 0x74,
|
||||||
|
0x72, 0x6f, 0x6c, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
|
||||||
|
0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x42, 0x40, 0x5a, 0x3e, 0x67,
|
||||||
|
0x69, 0x74, 0x2e, 0x6b, 0x36, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2f, 0x67, 0x6f, 0x2d, 0x63, 0x61,
|
||||||
|
0x72, 0x74, 0x2d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63,
|
||||||
|
0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x3b, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70,
|
||||||
|
0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x62, 0x06, 0x70,
|
||||||
|
0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
})
|
})
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -734,40 +750,40 @@ func file_control_plane_proto_rawDescGZIP() []byte {
|
|||||||
|
|
||||||
var file_control_plane_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
|
var file_control_plane_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
|
||||||
var file_control_plane_proto_goTypes = []any{
|
var file_control_plane_proto_goTypes = []any{
|
||||||
(*Empty)(nil), // 0: messages.Empty
|
(*Empty)(nil), // 0: control_plane_messages.Empty
|
||||||
(*PingReply)(nil), // 1: messages.PingReply
|
(*PingReply)(nil), // 1: control_plane_messages.PingReply
|
||||||
(*NegotiateRequest)(nil), // 2: messages.NegotiateRequest
|
(*NegotiateRequest)(nil), // 2: control_plane_messages.NegotiateRequest
|
||||||
(*NegotiateReply)(nil), // 3: messages.NegotiateReply
|
(*NegotiateReply)(nil), // 3: control_plane_messages.NegotiateReply
|
||||||
(*ActorIdsReply)(nil), // 4: messages.ActorIdsReply
|
(*ActorIdsReply)(nil), // 4: control_plane_messages.ActorIdsReply
|
||||||
(*OwnerChangeAck)(nil), // 5: messages.OwnerChangeAck
|
(*OwnerChangeAck)(nil), // 5: control_plane_messages.OwnerChangeAck
|
||||||
(*ClosingNotice)(nil), // 6: messages.ClosingNotice
|
(*ClosingNotice)(nil), // 6: control_plane_messages.ClosingNotice
|
||||||
(*OwnershipAnnounce)(nil), // 7: messages.OwnershipAnnounce
|
(*OwnershipAnnounce)(nil), // 7: control_plane_messages.OwnershipAnnounce
|
||||||
(*ExpiryAnnounce)(nil), // 8: messages.ExpiryAnnounce
|
(*ExpiryAnnounce)(nil), // 8: control_plane_messages.ExpiryAnnounce
|
||||||
(*ApplyRequest)(nil), // 9: messages.ApplyRequest
|
(*ApplyRequest)(nil), // 9: control_plane_messages.ApplyRequest
|
||||||
(*GetRequest)(nil), // 10: messages.GetRequest
|
(*GetRequest)(nil), // 10: control_plane_messages.GetRequest
|
||||||
(*GetReply)(nil), // 11: messages.GetReply
|
(*GetReply)(nil), // 11: control_plane_messages.GetReply
|
||||||
(*ApplyResult)(nil), // 12: messages.ApplyResult
|
(*ApplyResult)(nil), // 12: control_plane_messages.ApplyResult
|
||||||
(*anypb.Any)(nil), // 13: google.protobuf.Any
|
(*anypb.Any)(nil), // 13: google.protobuf.Any
|
||||||
}
|
}
|
||||||
var file_control_plane_proto_depIdxs = []int32{
|
var file_control_plane_proto_depIdxs = []int32{
|
||||||
13, // 0: messages.ApplyRequest.messages:type_name -> google.protobuf.Any
|
13, // 0: control_plane_messages.ApplyRequest.messages:type_name -> google.protobuf.Any
|
||||||
13, // 1: messages.GetReply.grain:type_name -> google.protobuf.Any
|
13, // 1: control_plane_messages.GetReply.grain:type_name -> google.protobuf.Any
|
||||||
0, // 2: messages.ControlPlane.Ping:input_type -> messages.Empty
|
0, // 2: control_plane_messages.ControlPlane.Ping:input_type -> control_plane_messages.Empty
|
||||||
2, // 3: messages.ControlPlane.Negotiate:input_type -> messages.NegotiateRequest
|
2, // 3: control_plane_messages.ControlPlane.Negotiate:input_type -> control_plane_messages.NegotiateRequest
|
||||||
0, // 4: messages.ControlPlane.GetLocalActorIds:input_type -> messages.Empty
|
0, // 4: control_plane_messages.ControlPlane.GetLocalActorIds:input_type -> control_plane_messages.Empty
|
||||||
7, // 5: messages.ControlPlane.AnnounceOwnership:input_type -> messages.OwnershipAnnounce
|
7, // 5: control_plane_messages.ControlPlane.AnnounceOwnership:input_type -> control_plane_messages.OwnershipAnnounce
|
||||||
9, // 6: messages.ControlPlane.Apply:input_type -> messages.ApplyRequest
|
9, // 6: control_plane_messages.ControlPlane.Apply:input_type -> control_plane_messages.ApplyRequest
|
||||||
8, // 7: messages.ControlPlane.AnnounceExpiry:input_type -> messages.ExpiryAnnounce
|
8, // 7: control_plane_messages.ControlPlane.AnnounceExpiry:input_type -> control_plane_messages.ExpiryAnnounce
|
||||||
6, // 8: messages.ControlPlane.Closing:input_type -> messages.ClosingNotice
|
6, // 8: control_plane_messages.ControlPlane.Closing:input_type -> control_plane_messages.ClosingNotice
|
||||||
10, // 9: messages.ControlPlane.Get:input_type -> messages.GetRequest
|
10, // 9: control_plane_messages.ControlPlane.Get:input_type -> control_plane_messages.GetRequest
|
||||||
1, // 10: messages.ControlPlane.Ping:output_type -> messages.PingReply
|
1, // 10: control_plane_messages.ControlPlane.Ping:output_type -> control_plane_messages.PingReply
|
||||||
3, // 11: messages.ControlPlane.Negotiate:output_type -> messages.NegotiateReply
|
3, // 11: control_plane_messages.ControlPlane.Negotiate:output_type -> control_plane_messages.NegotiateReply
|
||||||
4, // 12: messages.ControlPlane.GetLocalActorIds:output_type -> messages.ActorIdsReply
|
4, // 12: control_plane_messages.ControlPlane.GetLocalActorIds:output_type -> control_plane_messages.ActorIdsReply
|
||||||
5, // 13: messages.ControlPlane.AnnounceOwnership:output_type -> messages.OwnerChangeAck
|
5, // 13: control_plane_messages.ControlPlane.AnnounceOwnership:output_type -> control_plane_messages.OwnerChangeAck
|
||||||
12, // 14: messages.ControlPlane.Apply:output_type -> messages.ApplyResult
|
12, // 14: control_plane_messages.ControlPlane.Apply:output_type -> control_plane_messages.ApplyResult
|
||||||
5, // 15: messages.ControlPlane.AnnounceExpiry:output_type -> messages.OwnerChangeAck
|
5, // 15: control_plane_messages.ControlPlane.AnnounceExpiry:output_type -> control_plane_messages.OwnerChangeAck
|
||||||
5, // 16: messages.ControlPlane.Closing:output_type -> messages.OwnerChangeAck
|
5, // 16: control_plane_messages.ControlPlane.Closing:output_type -> control_plane_messages.OwnerChangeAck
|
||||||
11, // 17: messages.ControlPlane.Get:output_type -> messages.GetReply
|
11, // 17: control_plane_messages.ControlPlane.Get:output_type -> control_plane_messages.GetReply
|
||||||
10, // [10:18] is the sub-list for method output_type
|
10, // [10:18] is the sub-list for method output_type
|
||||||
2, // [2:10] is the sub-list for method input_type
|
2, // [2:10] is the sub-list for method input_type
|
||||||
2, // [2:2] is the sub-list for extension type_name
|
2, // [2:2] is the sub-list for extension type_name
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
// - protoc v6.33.1
|
// - protoc v6.33.1
|
||||||
// source: control_plane.proto
|
// source: control_plane.proto
|
||||||
|
|
||||||
package messages
|
package control_plane_messages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
context "context"
|
context "context"
|
||||||
@@ -19,14 +19,14 @@ import (
|
|||||||
const _ = grpc.SupportPackageIsVersion9
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ControlPlane_Ping_FullMethodName = "/messages.ControlPlane/Ping"
|
ControlPlane_Ping_FullMethodName = "/control_plane_messages.ControlPlane/Ping"
|
||||||
ControlPlane_Negotiate_FullMethodName = "/messages.ControlPlane/Negotiate"
|
ControlPlane_Negotiate_FullMethodName = "/control_plane_messages.ControlPlane/Negotiate"
|
||||||
ControlPlane_GetLocalActorIds_FullMethodName = "/messages.ControlPlane/GetLocalActorIds"
|
ControlPlane_GetLocalActorIds_FullMethodName = "/control_plane_messages.ControlPlane/GetLocalActorIds"
|
||||||
ControlPlane_AnnounceOwnership_FullMethodName = "/messages.ControlPlane/AnnounceOwnership"
|
ControlPlane_AnnounceOwnership_FullMethodName = "/control_plane_messages.ControlPlane/AnnounceOwnership"
|
||||||
ControlPlane_Apply_FullMethodName = "/messages.ControlPlane/Apply"
|
ControlPlane_Apply_FullMethodName = "/control_plane_messages.ControlPlane/Apply"
|
||||||
ControlPlane_AnnounceExpiry_FullMethodName = "/messages.ControlPlane/AnnounceExpiry"
|
ControlPlane_AnnounceExpiry_FullMethodName = "/control_plane_messages.ControlPlane/AnnounceExpiry"
|
||||||
ControlPlane_Closing_FullMethodName = "/messages.ControlPlane/Closing"
|
ControlPlane_Closing_FullMethodName = "/control_plane_messages.ControlPlane/Closing"
|
||||||
ControlPlane_Get_FullMethodName = "/messages.ControlPlane/Get"
|
ControlPlane_Get_FullMethodName = "/control_plane_messages.ControlPlane/Get"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ControlPlaneClient is the client API for ControlPlane service.
|
// ControlPlaneClient is the client API for ControlPlane service.
|
||||||
@@ -362,7 +362,7 @@ func _ControlPlane_Get_Handler(srv interface{}, ctx context.Context, dec func(in
|
|||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
var ControlPlane_ServiceDesc = grpc.ServiceDesc{
|
var ControlPlane_ServiceDesc = grpc.ServiceDesc{
|
||||||
ServiceName: "messages.ControlPlane",
|
ServiceName: "control_plane_messages.ControlPlane",
|
||||||
HandlerType: (*ControlPlaneServer)(nil),
|
HandlerType: (*ControlPlaneServer)(nil),
|
||||||
Methods: []grpc.MethodDesc{
|
Methods: []grpc.MethodDesc{
|
||||||
{
|
{
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
|
|
||||||
package messages;
|
package control_plane_messages;
|
||||||
|
|
||||||
option go_package = "git.k6n.net/go-cart-actor/proto;messages";
|
option go_package = "git.k6n.net/go-cart-actor/proto/control;control_plane_messages";
|
||||||
|
|
||||||
import "google/protobuf/any.proto";
|
import "google/protobuf/any.proto";
|
||||||
|
|
||||||
|
|||||||
@@ -1,232 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
package messages;
|
|
||||||
option go_package = "git.k6n.net/go-cart-actor/proto;messages";
|
|
||||||
|
|
||||||
import "google/protobuf/any.proto";
|
|
||||||
import "google/protobuf/timestamp.proto";
|
|
||||||
|
|
||||||
message ClearCartRequest {}
|
|
||||||
|
|
||||||
message AddItem {
|
|
||||||
uint32 item_id = 1;
|
|
||||||
int32 quantity = 2;
|
|
||||||
int64 price = 3;
|
|
||||||
int64 orgPrice = 9;
|
|
||||||
string sku = 4;
|
|
||||||
string name = 5;
|
|
||||||
string image = 6;
|
|
||||||
int32 stock = 7;
|
|
||||||
int32 tax = 8;
|
|
||||||
string brand = 13;
|
|
||||||
string category = 14;
|
|
||||||
string category2 = 15;
|
|
||||||
string category3 = 16;
|
|
||||||
string category4 = 17;
|
|
||||||
string category5 = 18;
|
|
||||||
string disclaimer = 10;
|
|
||||||
string articleType = 11;
|
|
||||||
string sellerId = 19;
|
|
||||||
string sellerName = 20;
|
|
||||||
string country = 21;
|
|
||||||
string saleStatus = 24;
|
|
||||||
optional string outlet = 12;
|
|
||||||
optional string storeId = 22;
|
|
||||||
optional uint32 parentId = 23;
|
|
||||||
string cgm = 25;
|
|
||||||
optional google.protobuf.Timestamp reservationEndTime = 26;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RemoveItem { uint32 Id = 1; }
|
|
||||||
|
|
||||||
message ChangeQuantity {
|
|
||||||
uint32 Id = 1;
|
|
||||||
int32 quantity = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SetDelivery {
|
|
||||||
string provider = 1;
|
|
||||||
repeated uint32 items = 2;
|
|
||||||
optional PickupPoint pickupPoint = 3;
|
|
||||||
string country = 4;
|
|
||||||
string zip = 5;
|
|
||||||
optional string address = 6;
|
|
||||||
optional string city = 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SetPickupPoint {
|
|
||||||
uint32 deliveryId = 1;
|
|
||||||
string id = 2;
|
|
||||||
optional string name = 3;
|
|
||||||
optional string address = 4;
|
|
||||||
optional string city = 5;
|
|
||||||
optional string zip = 6;
|
|
||||||
optional string country = 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
message PickupPoint {
|
|
||||||
string id = 1;
|
|
||||||
optional string name = 2;
|
|
||||||
optional string address = 3;
|
|
||||||
optional string city = 4;
|
|
||||||
optional string zip = 5;
|
|
||||||
optional string country = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RemoveDelivery { uint32 id = 1; }
|
|
||||||
|
|
||||||
message SetUserId {
|
|
||||||
string userId = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message LineItemMarking {
|
|
||||||
uint32 id = 1;
|
|
||||||
uint32 type = 2;
|
|
||||||
string marking = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RemoveLineItemMarking {
|
|
||||||
uint32 id = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SubscriptionAdded {
|
|
||||||
uint32 itemId = 1;
|
|
||||||
string detailsId = 3;
|
|
||||||
string orderReference = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message PaymentStarted {
|
|
||||||
string paymentId = 1;
|
|
||||||
uint32 version = 2;
|
|
||||||
int64 amount = 3;
|
|
||||||
string currency = 4;
|
|
||||||
string provider = 5;
|
|
||||||
optional string method = 6;
|
|
||||||
optional google.protobuf.Timestamp startedAt = 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
message PaymentCompleted {
|
|
||||||
string paymentId = 1;
|
|
||||||
string status = 2;
|
|
||||||
int64 amount = 3;
|
|
||||||
string currency = 4;
|
|
||||||
optional string processorReference = 5;
|
|
||||||
optional google.protobuf.Timestamp completedAt = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
message PaymentDeclined {
|
|
||||||
string paymentId = 1;
|
|
||||||
string message = 2;
|
|
||||||
optional string code = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message PaymentEvent {
|
|
||||||
string paymentId = 1;
|
|
||||||
string name = 2;
|
|
||||||
bool success = 3;
|
|
||||||
google.protobuf.Any data = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ConfirmationViewed {
|
|
||||||
google.protobuf.Timestamp viewedAt = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message CreateCheckoutOrder {
|
|
||||||
string terms = 1;
|
|
||||||
string checkout = 2;
|
|
||||||
string confirmation = 3;
|
|
||||||
string push = 4;
|
|
||||||
string validation = 5;
|
|
||||||
string country = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
message OrderCreated {
|
|
||||||
string orderId = 1;
|
|
||||||
string status = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Noop {
|
|
||||||
// Intentionally empty - used for ownership acquisition or health pings
|
|
||||||
}
|
|
||||||
|
|
||||||
message InitializeCheckout {
|
|
||||||
string orderId = 1;
|
|
||||||
string status = 2;
|
|
||||||
bool paymentInProgress = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message InventoryReserved {
|
|
||||||
string id = 1;
|
|
||||||
string status = 2;
|
|
||||||
optional string message = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message AddVoucher {
|
|
||||||
string code = 1;
|
|
||||||
int64 value = 2;
|
|
||||||
repeated string voucherRules = 3;
|
|
||||||
string description = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RemoveVoucher { uint32 id = 1; }
|
|
||||||
|
|
||||||
message UpsertSubscriptionDetails {
|
|
||||||
optional string id = 1;
|
|
||||||
string offeringCode = 2;
|
|
||||||
string signingType = 3;
|
|
||||||
google.protobuf.Any data = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message PreConditionFailed {
|
|
||||||
string operation = 1;
|
|
||||||
string error = 2;
|
|
||||||
google.protobuf.Any input = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message GiftcardItem {
|
|
||||||
int64 value = 1;
|
|
||||||
string deliveryDate = 2;
|
|
||||||
string recipient = 3;
|
|
||||||
string recipientType = 4;
|
|
||||||
string message = 5;
|
|
||||||
optional google.protobuf.Any designConfig = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
message AddGiftcard {
|
|
||||||
GiftcardItem giftcard = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RemoveGiftcard {
|
|
||||||
uint32 id = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Mutation {
|
|
||||||
oneof type {
|
|
||||||
ClearCartRequest clear_cart = 1;
|
|
||||||
AddItem add_item = 2;
|
|
||||||
RemoveItem remove_item = 3;
|
|
||||||
ChangeQuantity change_quantity = 4;
|
|
||||||
SetDelivery set_delivery = 5;
|
|
||||||
SetPickupPoint set_pickup_point = 6;
|
|
||||||
RemoveDelivery remove_delivery = 7;
|
|
||||||
SetUserId set_user_id = 8;
|
|
||||||
LineItemMarking line_item_marking = 9;
|
|
||||||
RemoveLineItemMarking remove_line_item_marking = 10;
|
|
||||||
SubscriptionAdded subscription_added = 11;
|
|
||||||
PaymentDeclined payment_declined = 12;
|
|
||||||
ConfirmationViewed confirmation_viewed = 13;
|
|
||||||
CreateCheckoutOrder create_checkout_order = 14;
|
|
||||||
OrderCreated order_created = 15;
|
|
||||||
Noop noop = 16;
|
|
||||||
InitializeCheckout initialize_checkout = 17;
|
|
||||||
InventoryReserved inventory_reserved = 18;
|
|
||||||
AddVoucher add_voucher = 19;
|
|
||||||
RemoveVoucher remove_voucher = 20;
|
|
||||||
UpsertSubscriptionDetails upsert_subscription_details = 21;
|
|
||||||
PreConditionFailed pre_condition_failed = 22;
|
|
||||||
AddGiftcard add_giftcard = 23;
|
|
||||||
RemoveGiftcard remove_giftcard = 24;
|
|
||||||
PaymentStarted payment_started = 25;
|
|
||||||
PaymentCompleted payment_completed = 26;
|
|
||||||
PaymentEvent payment_event = 27;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user