Compare commits
108
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e36af2524 | ||
|
|
d78685cd8f | ||
|
|
f67eeb3c49 | ||
|
|
6d5358b53b | ||
|
|
d0d604f5f0 | ||
|
|
2b8e692526 | ||
|
|
c842571dbc | ||
|
|
bd194f2bb8 | ||
|
|
94d478ee6a | ||
|
|
e9fb00d5c7 | ||
|
|
3d9d703a76 | ||
|
|
9a61b088c9 | ||
|
|
3becd1c80c | ||
|
|
dcc7cc2dd0 | ||
|
|
ce81e294f2 | ||
|
|
fccb0eed06 | ||
|
|
978ef3025d | ||
|
|
0ef29596c1 | ||
|
|
fbc773dbca | ||
|
|
5037c07a47 | ||
|
|
e9189364ab | ||
|
|
445858c1e3 | ||
|
|
82b64ee8d4 | ||
|
|
7606c1f9df | ||
|
|
0e1f9b0d3c | ||
|
|
95d8e0ffb5 | ||
|
|
c61fe1bb7b | ||
|
|
3af837d824 | ||
|
|
401286c56a | ||
|
|
bb4ed5eb23 | ||
|
|
f828f4809b | ||
|
|
825630a91a | ||
|
|
78bc05025d | ||
|
|
5ad7131df2 | ||
|
|
6321ad76a1 | ||
|
|
94a04a154c | ||
|
|
86fdfc8532 | ||
|
|
c57643ab63 | ||
|
|
9fde6d6bd7 | ||
|
|
1cca11de35 | ||
|
|
1b4e769573 | ||
|
|
8f2a354f9d | ||
|
|
4940b61ee0 | ||
|
|
ad24d503ba | ||
|
|
ca7d23f122 | ||
|
|
c7049793e2 | ||
|
|
dbdd759e16 | ||
|
|
ee5f54f0dd | ||
|
|
ebd1508294 | ||
|
|
d8791788fc | ||
|
|
ee1b96fece | ||
|
|
060b3dfbf0 | ||
|
|
c227870f13 | ||
|
|
d23bfe62a1 | ||
|
|
accdc41064 | ||
|
|
e419cde351 | ||
|
|
f0c487b5e1 | ||
|
|
84268eede4 | ||
|
|
aaa730e833 | ||
|
|
c11161b0ca | ||
|
|
cdb678f018 | ||
|
|
c9695d338d | ||
|
|
e1072c5ba1 | ||
|
|
aea2adcf92 | ||
|
|
90a525bd98 | ||
|
|
330093bdec | ||
|
|
47e69f18a5 | ||
|
|
2c0f6c160a | ||
|
|
23160479b3 | ||
|
|
d8db3c9289 | ||
|
|
43b7e749c4 | ||
|
|
c3d8773c2e | ||
|
|
c2873ebfc0 | ||
|
|
db61413988 | ||
|
|
65dc8edf48 | ||
|
|
fd29300314 | ||
|
|
aea168160e | ||
|
|
c2a137d8d4 | ||
|
|
51d7de03d6 | ||
|
|
3912135b35 | ||
|
|
afa79e7b56 | ||
|
|
a17d5fc570 | ||
|
|
fac94acfe1 | ||
|
|
beb03a7cd7 | ||
|
|
87625ffd5d | ||
|
|
9db4abdb05 | ||
|
|
a6f6d3c190 | ||
|
|
400079ec98 | ||
|
|
95426acd4a | ||
|
|
7e2726244f | ||
|
|
59e01e763f | ||
|
|
d5d2b3e711 | ||
|
|
f640cd7d2c | ||
|
|
82650bd3dd | ||
|
|
990708597b | ||
|
|
e7e572b0f7 | ||
|
|
b0907aee41 | ||
|
|
ff8a332e3b | ||
|
|
4ca6c4ca23 | ||
|
|
e74f5ae30e | ||
|
|
8d976dd250 | ||
|
|
fc678797ba | ||
|
|
374bd4272b | ||
|
|
ffd7ace839 | ||
|
|
c74968cfce | ||
|
|
f63ad5bc84 | ||
|
|
4dcc6fe1bd | ||
|
|
886e3ce48f |
@@ -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:
|
||||||
@@ -22,6 +25,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
kubectl rollout restart deployment/cart-backoffice-x86 -n cart
|
kubectl rollout restart deployment/cart-backoffice-x86 -n cart
|
||||||
kubectl rollout restart deployment/cart-actor-x86 -n cart
|
kubectl rollout restart deployment/cart-actor-x86 -n cart
|
||||||
|
kubectl rollout restart deployment/checkout-actor-x86 -n cart
|
||||||
|
|
||||||
BuildAndDeployArm64:
|
BuildAndDeployArm64:
|
||||||
runs-on: arm64
|
runs-on: arm64
|
||||||
|
|||||||
+13
-8
@@ -25,6 +25,8 @@
|
|||||||
FROM golang:1.25-alpine AS build
|
FROM golang:1.25-alpine AS build
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
# Build metadata (can be overridden at build time)
|
# Build metadata (can be overridden at build time)
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ARG GIT_COMMIT=unknown
|
ARG GIT_COMMIT=unknown
|
||||||
@@ -38,8 +40,7 @@ ENV CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH}
|
|||||||
|
|
||||||
# Dependency caching
|
# Dependency caching
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
RUN go mod download
|
||||||
go mod download
|
|
||||||
|
|
||||||
# Copy full source (relay on .dockerignore to prune)
|
# Copy full source (relay on .dockerignore to prune)
|
||||||
COPY . .
|
COPY . .
|
||||||
@@ -52,27 +53,30 @@ COPY . .
|
|||||||
# proto/*.proto
|
# proto/*.proto
|
||||||
|
|
||||||
# Build with minimal binary size and embedded metadata
|
# Build with minimal binary size and embedded metadata
|
||||||
RUN --mount=type=cache,target=/go/build-cache \
|
RUN go build -trimpath -ldflags="-s -w \
|
||||||
go build -trimpath -ldflags="-s -w \
|
|
||||||
-X main.Version=${VERSION} \
|
-X main.Version=${VERSION} \
|
||||||
-X main.GitCommit=${GIT_COMMIT} \
|
-X main.GitCommit=${GIT_COMMIT} \
|
||||||
-X main.BuildDate=${BUILD_DATE}" \
|
-X main.BuildDate=${BUILD_DATE}" \
|
||||||
-o /out/go-cart-actor ./cmd/cart
|
-o /out/go-cart-actor ./cmd/cart
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/go/build-cache \
|
RUN go build -trimpath -ldflags="-s -w \
|
||||||
go build -trimpath -ldflags="-s -w \
|
|
||||||
-X main.Version=${VERSION} \
|
-X main.Version=${VERSION} \
|
||||||
-X main.GitCommit=${GIT_COMMIT} \
|
-X main.GitCommit=${GIT_COMMIT} \
|
||||||
-X main.BuildDate=${BUILD_DATE}" \
|
-X main.BuildDate=${BUILD_DATE}" \
|
||||||
-o /out/go-cart-backoffice ./cmd/backoffice
|
-o /out/go-cart-backoffice ./cmd/backoffice
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/go/build-cache \
|
RUN go build -trimpath -ldflags="-s -w \
|
||||||
go build -trimpath -ldflags="-s -w \
|
|
||||||
-X main.Version=${VERSION} \
|
-X main.Version=${VERSION} \
|
||||||
-X main.GitCommit=${GIT_COMMIT} \
|
-X main.GitCommit=${GIT_COMMIT} \
|
||||||
-X main.BuildDate=${BUILD_DATE}" \
|
-X main.BuildDate=${BUILD_DATE}" \
|
||||||
-o /out/go-cart-inventory ./cmd/inventory
|
-o /out/go-cart-inventory ./cmd/inventory
|
||||||
|
|
||||||
|
RUN go build -trimpath -ldflags="-s -w \
|
||||||
|
-X main.Version=${VERSION} \
|
||||||
|
-X main.GitCommit=${GIT_COMMIT} \
|
||||||
|
-X main.BuildDate=${BUILD_DATE}" \
|
||||||
|
-o /out/go-checkout-actor ./cmd/checkout
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# Runtime Stage
|
# Runtime Stage
|
||||||
############################
|
############################
|
||||||
@@ -81,6 +85,7 @@ FROM gcr.io/distroless/static-debian12:nonroot AS runtime
|
|||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|
||||||
COPY --from=build /out/go-cart-actor /go-cart-actor
|
COPY --from=build /out/go-cart-actor /go-cart-actor
|
||||||
|
COPY --from=build /out/go-checkout-actor /go-checkout-actor
|
||||||
COPY --from=build /out/go-cart-backoffice /go-cart-backoffice
|
COPY --from=build /out/go-cart-backoffice /go-cart-backoffice
|
||||||
COPY --from=build /out/go-cart-inventory /go-cart-inventory
|
COPY --from=build /out/go-cart-inventory /go-cart-inventory
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,15 @@
|
|||||||
# Conventions:
|
# Conventions:
|
||||||
# - All .proto files live in $(PROTO_DIR)
|
# - All .proto files live in $(PROTO_DIR)
|
||||||
# - Generated Go code is emitted under $(PROTO_DIR) via go_package mapping
|
# - Generated Go code is emitted under $(PROTO_DIR) via go_package mapping
|
||||||
# - go_package is set to: git.tornberg.me/go-cart-actor/proto;messages
|
# - go_package is set to: git.k6n.net/go-cart-actor/proto;messages
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
MODULE_PATH := git.tornberg.me/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,22 +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 -rf git.tornberg.me
|
@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
|
||||||
|
|||||||
@@ -16,21 +16,26 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.tornberg.me/go-cart-actor/pkg/actor"
|
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||||
"git.tornberg.me/go-cart-actor/pkg/cart"
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
"github.com/gogo/protobuf/proto"
|
"git.k6n.net/go-cart-actor/pkg/checkout"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FileServer struct {
|
type FileServer struct {
|
||||||
// Define fields here
|
// Define fields here
|
||||||
dataDir string
|
dataDir string
|
||||||
storage actor.LogStorage[cart.CartGrain]
|
checkoutDataDir string
|
||||||
|
storage actor.LogStorage[cart.CartGrain]
|
||||||
|
checkoutStorage actor.LogStorage[checkout.CheckoutGrain]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFileServer(dataDir string, storage actor.LogStorage[cart.CartGrain]) *FileServer {
|
func NewFileServer(dataDir string, checkoutDataDir string, storage actor.LogStorage[cart.CartGrain], checkoutStorage actor.LogStorage[checkout.CheckoutGrain]) *FileServer {
|
||||||
return &FileServer{
|
return &FileServer{
|
||||||
dataDir: dataDir,
|
dataDir: dataDir,
|
||||||
storage: storage,
|
checkoutDataDir: checkoutDataDir,
|
||||||
|
storage: storage,
|
||||||
|
checkoutStorage: checkoutStorage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +84,12 @@ func appendFileInfo(info fs.FileInfo, out *CartFileInfo) *CartFileInfo {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func appendCheckoutFileInfo(info fs.FileInfo, out *CheckoutFileInfo) *CheckoutFileInfo {
|
||||||
|
out.Size = info.Size()
|
||||||
|
out.Modified = info.ModTime()
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// var cartFileRe = regexp.MustCompile(`^(\d+)\.events\.log$`)
|
// var cartFileRe = regexp.MustCompile(`^(\d+)\.events\.log$`)
|
||||||
|
|
||||||
func listCartFiles(dir string) ([]*CartFileInfo, error) {
|
func listCartFiles(dir string) ([]*CartFileInfo, error) {
|
||||||
@@ -112,6 +123,36 @@ func listCartFiles(dir string) ([]*CartFileInfo, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func listCheckoutFiles(dir string) ([]*CheckoutFileInfo, error) {
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return []*CheckoutFileInfo{}, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out := make([]*CheckoutFileInfo, 0)
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id, valid := isValidFileId(e.Name())
|
||||||
|
if !valid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := e.Info()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, appendCheckoutFileInfo(info, &CheckoutFileInfo{
|
||||||
|
ID: fmt.Sprintf("%d", id),
|
||||||
|
CheckoutId: checkout.CheckoutId(id),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func readRawLogLines(path string) ([]json.RawMessage, error) {
|
func readRawLogLines(path string) ([]json.RawMessage, error) {
|
||||||
fh, err := os.Open(path)
|
fh, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -158,6 +199,21 @@ func (fs *FileServer) CartsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fs *FileServer) CheckoutsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
list, err := listCheckoutFiles(fs.checkoutDataDir)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// sort by modified desc
|
||||||
|
sort.Slice(list, func(i, j int) bool { return list[i].Modified.After(list[j].Modified) })
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{
|
||||||
|
"count": len(list),
|
||||||
|
"checkouts": list,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (fs *FileServer) PromotionsHandler(w http.ResponseWriter, r *http.Request) {
|
func (fs *FileServer) PromotionsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
fileName := filepath.Join(fs.dataDir, "promotions.json")
|
fileName := filepath.Join(fs.dataDir, "promotions.json")
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
@@ -315,3 +371,69 @@ func (fs *FileServer) CartHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fs *FileServer) CheckoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
idStr := r.PathValue("id")
|
||||||
|
if idStr == "" {
|
||||||
|
writeJSON(w, http.StatusBadRequest, JsonError{Error: "missing id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, ok := isValidId(idStr)
|
||||||
|
if !ok {
|
||||||
|
writeJSON(w, http.StatusBadRequest, JsonError{Error: "invalid id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// parse query parameters for filtering
|
||||||
|
query := r.URL.Query()
|
||||||
|
filterFunction := acceptAll
|
||||||
|
if maxIndexStr := query.Get("maxIndex"); maxIndexStr != "" {
|
||||||
|
log.Printf("filter maxIndex: %s", maxIndexStr)
|
||||||
|
maxIndex, err := strconv.Atoi(maxIndexStr)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusBadRequest, JsonError{Error: "invalid maxIndex"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filterFunction = acceptUntilIndex(maxIndex)
|
||||||
|
} else if untilStr := query.Get("until"); untilStr != "" {
|
||||||
|
log.Printf("filter until: %s", untilStr)
|
||||||
|
until, err := time.Parse(time.RFC3339, untilStr)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusBadRequest, JsonError{Error: "invalid until timestamp"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filterFunction = acceptUntilTimestamp(until)
|
||||||
|
}
|
||||||
|
// reconstruct state from event log if present
|
||||||
|
grain := checkout.NewCheckoutGrain(id, cart.CartId(id), 0, time.Now(), nil)
|
||||||
|
err := fs.checkoutStorage.LoadEventsFunc(r.Context(), id, grain, filterFunction)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusInternalServerError, JsonError{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(fs.checkoutDataDir, fmt.Sprintf("%d.events.log", id))
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil && errors.Is(err, os.ErrNotExist) {
|
||||||
|
writeJSON(w, http.StatusNotFound, JsonError{Error: "checkout not found"})
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
writeJSON(w, http.StatusInternalServerError, JsonError{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lines, err := readRawLogLines(path)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusInternalServerError, JsonError{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, map[string]any{
|
||||||
|
"id": id,
|
||||||
|
"checkoutId": checkout.CheckoutId(id).String(),
|
||||||
|
"state": grain,
|
||||||
|
"mutations": lines,
|
||||||
|
"meta": map[string]any{
|
||||||
|
"size": info.Size(),
|
||||||
|
"modified": info.ModTime(),
|
||||||
|
"path": path,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.tornberg.me/go-cart-actor/pkg/cart"
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestAppendFileInfoRandomProjectFile picks a random existing .go source file in the
|
// TestAppendFileInfoRandomProjectFile picks a random existing .go source file in the
|
||||||
|
|||||||
+62
-4
@@ -2,16 +2,20 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
actor "git.tornberg.me/go-cart-actor/pkg/actor"
|
actor "git.k6n.net/go-cart-actor/pkg/actor"
|
||||||
"git.tornberg.me/go-cart-actor/pkg/cart"
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/checkout"
|
||||||
|
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
||||||
"github.com/matst80/slask-finder/pkg/messaging"
|
"github.com/matst80/slask-finder/pkg/messaging"
|
||||||
amqp "github.com/rabbitmq/amqp091-go"
|
amqp "github.com/rabbitmq/amqp091-go"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CartFileInfo struct {
|
type CartFileInfo struct {
|
||||||
@@ -21,6 +25,13 @@ type CartFileInfo struct {
|
|||||||
Modified time.Time `json:"modified"`
|
Modified time.Time `json:"modified"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CheckoutFileInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
CheckoutId checkout.CheckoutId `json:"checkoutId"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
Modified time.Time `json:"modified"`
|
||||||
|
}
|
||||||
|
|
||||||
func envOrDefault(key, def string) string {
|
func envOrDefault(key, def string) string {
|
||||||
if v := os.Getenv(key); v != "" {
|
if v := os.Getenv(key); v != "" {
|
||||||
return v
|
return v
|
||||||
@@ -71,17 +82,36 @@ func startMutationConsumer(ctx context.Context, conn *amqp.Connection, hub *Hub)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var redisAddress = os.Getenv("REDIS_ADDRESS")
|
||||||
|
var redisPassword = os.Getenv("REDIS_PASSWORD")
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
dataDir := envOrDefault("DATA_DIR", "data")
|
dataDir := envOrDefault("DATA_DIR", "data")
|
||||||
addr := envOrDefault("ADDR", ":8080")
|
addr := envOrDefault("ADDR", ":8080")
|
||||||
amqpURL := os.Getenv("AMQP_URL")
|
amqpURL := os.Getenv("AMQP_URL")
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
_ = os.MkdirAll(dataDir, 0755)
|
_ = os.MkdirAll(dataDir, 0755)
|
||||||
|
|
||||||
reg := cart.NewCartMultationRegistry()
|
reg := cart.NewCartMultationRegistry(cart.NewCartMutationContext(nil))
|
||||||
diskStorage := actor.NewDiskStorage[cart.CartGrain](dataDir, reg)
|
diskStorage := actor.NewDiskStorage[cart.CartGrain](dataDir, reg)
|
||||||
|
|
||||||
fs := NewFileServer(dataDir, diskStorage)
|
checkoutDataDir := envOrDefault("CHECKOUT_DATA_DIR", "checkout-data")
|
||||||
|
_ = os.MkdirAll(checkoutDataDir, 0755)
|
||||||
|
|
||||||
|
regCheckout := checkout.NewCheckoutMutationRegistry(checkout.NewCheckoutMutationContext())
|
||||||
|
diskStorageCheckout := actor.NewDiskStorage[checkout.CheckoutGrain](checkoutDataDir, regCheckout)
|
||||||
|
|
||||||
|
fs := NewFileServer(dataDir, checkoutDataDir, diskStorage, diskStorageCheckout)
|
||||||
|
|
||||||
hub := NewHub()
|
hub := NewHub()
|
||||||
go hub.Run()
|
go hub.Run()
|
||||||
@@ -89,6 +119,34 @@ func main() {
|
|||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("GET /carts", fs.CartsHandler)
|
mux.HandleFunc("GET /carts", fs.CartsHandler)
|
||||||
mux.HandleFunc("GET /cart/{id}", fs.CartHandler)
|
mux.HandleFunc("GET /cart/{id}", fs.CartHandler)
|
||||||
|
mux.HandleFunc("GET /checkouts", fs.CheckoutsHandler)
|
||||||
|
mux.HandleFunc("GET /checkout/{id}", fs.CheckoutHandler)
|
||||||
|
mux.HandleFunc("PUT /inventory/{locationId}/{sku}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
inventoryLocationId := inventory.LocationID(r.PathValue("locationId"))
|
||||||
|
inventorySku := inventory.SKU(r.PathValue("sku"))
|
||||||
|
pipe := rdb.Pipeline()
|
||||||
|
var payload struct {
|
||||||
|
Quantity int64 `json:"quantity"`
|
||||||
|
}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&payload)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid payload", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
inventoryService.UpdateInventory(r.Context(), pipe, inventorySku, inventoryLocationId, payload.Quantity)
|
||||||
|
|
||||||
|
_, err = pipe.Exec(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to update inventory", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = inventoryService.SendInventoryChanged(r.Context(), inventorySku, inventoryLocationId)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
mux.HandleFunc("/promotions", fs.PromotionsHandler)
|
mux.HandleFunc("/promotions", fs.PromotionsHandler)
|
||||||
mux.HandleFunc("/vouchers", fs.VoucherHandler)
|
mux.HandleFunc("/vouchers", fs.VoucherHandler)
|
||||||
mux.HandleFunc("/promotion/{id}", fs.PromotionPartHandler)
|
mux.HandleFunc("/promotion/{id}", fs.PromotionPartHandler)
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.tornberg.me/go-cart-actor/pkg/cart"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CheckoutMeta carries the external / URL metadata required to build a
|
|
||||||
// Klarna CheckoutOrder from a CartGrain snapshot. It deliberately excludes
|
|
||||||
// any Klarna-specific response fields (HTML snippet, client token, etc.).
|
|
||||||
type CheckoutMeta struct {
|
|
||||||
Terms string
|
|
||||||
Checkout string
|
|
||||||
Confirmation string
|
|
||||||
Country string
|
|
||||||
Currency string // optional override (defaults to "SEK" if empty)
|
|
||||||
Locale string // optional override (defaults to "sv-se" if empty)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildCheckoutOrderPayload converts the current cart grain + meta information
|
|
||||||
// into a CheckoutOrder domain struct and returns its JSON-serialized payload
|
|
||||||
// (to send to Klarna) alongside the structured CheckoutOrder object.
|
|
||||||
//
|
|
||||||
// This function is PURE: it does not perform any network I/O or mutate the
|
|
||||||
// grain. The caller is responsible for:
|
|
||||||
//
|
|
||||||
// 1. Choosing whether to create or update the Klarna order.
|
|
||||||
// 2. Invoking KlarnaClient.CreateOrder / UpdateOrder with the returned payload.
|
|
||||||
// 3. Applying an InitializeCheckout mutation (or equivalent) with the
|
|
||||||
// resulting Klarna order id + status.
|
|
||||||
//
|
|
||||||
// If you later need to support different tax rates per line, you can extend
|
|
||||||
// CartItem / Delivery to expose that data and propagate it here.
|
|
||||||
func BuildCheckoutOrderPayload(grain *cart.CartGrain, meta *CheckoutMeta) ([]byte, *CheckoutOrder, error) {
|
|
||||||
if grain == nil {
|
|
||||||
return nil, nil, fmt.Errorf("nil grain")
|
|
||||||
}
|
|
||||||
if meta == nil {
|
|
||||||
return nil, nil, fmt.Errorf("nil checkout meta")
|
|
||||||
}
|
|
||||||
|
|
||||||
currency := meta.Currency
|
|
||||||
if currency == "" {
|
|
||||||
currency = "SEK"
|
|
||||||
}
|
|
||||||
locale := meta.Locale
|
|
||||||
if locale == "" {
|
|
||||||
locale = "sv-se"
|
|
||||||
}
|
|
||||||
country := meta.Country
|
|
||||||
if country == "" {
|
|
||||||
country = "SE" // sensible default; adjust if multi-country support changes
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := make([]*Line, 0, len(grain.Items)+len(grain.Deliveries))
|
|
||||||
|
|
||||||
// Item lines
|
|
||||||
for _, it := range grain.Items {
|
|
||||||
if it == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
lines = append(lines, &Line{
|
|
||||||
Type: "physical",
|
|
||||||
Reference: it.Sku,
|
|
||||||
Name: it.Meta.Name,
|
|
||||||
Quantity: it.Quantity,
|
|
||||||
UnitPrice: int(it.Price.IncVat),
|
|
||||||
TaxRate: it.Tax, // TODO: derive if variable tax rates are introduced
|
|
||||||
QuantityUnit: "st",
|
|
||||||
TotalAmount: int(it.TotalPrice.IncVat),
|
|
||||||
TotalTaxAmount: int(it.TotalPrice.TotalVat()),
|
|
||||||
ImageURL: fmt.Sprintf("https://www.elgiganten.se%s", it.Meta.Image),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delivery lines
|
|
||||||
for _, d := range grain.Deliveries {
|
|
||||||
if d == nil || d.Price.IncVat <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
lines = append(lines, &Line{
|
|
||||||
Type: "shipping_fee",
|
|
||||||
Reference: d.Provider,
|
|
||||||
Name: "Delivery",
|
|
||||||
Quantity: 1,
|
|
||||||
UnitPrice: int(d.Price.IncVat),
|
|
||||||
TaxRate: 2500,
|
|
||||||
QuantityUnit: "st",
|
|
||||||
TotalAmount: int(d.Price.IncVat),
|
|
||||||
TotalTaxAmount: int(d.Price.TotalVat()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
order := &CheckoutOrder{
|
|
||||||
PurchaseCountry: country,
|
|
||||||
PurchaseCurrency: currency,
|
|
||||||
Locale: locale,
|
|
||||||
OrderAmount: int(grain.TotalPrice.IncVat),
|
|
||||||
OrderTaxAmount: int(grain.TotalPrice.TotalVat()),
|
|
||||||
OrderLines: lines,
|
|
||||||
MerchantReference1: grain.Id.String(),
|
|
||||||
MerchantURLS: &CheckoutMerchantURLS{
|
|
||||||
Terms: meta.Terms,
|
|
||||||
Checkout: meta.Checkout,
|
|
||||||
Confirmation: meta.Confirmation,
|
|
||||||
Notification: "https://cart.k6n.net/notification",
|
|
||||||
Validation: "https://cart.k6n.net/validate",
|
|
||||||
Push: "https://cart.k6n.net/push?order_id={checkout.order.id}",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
payload, err := json.Marshal(order)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("marshal checkout order: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return payload, order, nil
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.tornberg.me/go-cart-actor/pkg/actor"
|
|
||||||
"git.tornberg.me/go-cart-actor/pkg/cart"
|
|
||||||
"git.tornberg.me/go-cart-actor/pkg/messages"
|
|
||||||
"git.tornberg.me/mats/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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -3,9 +3,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"git.tornberg.me/go-cart-actor/pkg/actor"
|
"git.k6n.net/go-cart-actor/pkg/discovery"
|
||||||
"git.tornberg.me/go-cart-actor/pkg/cart"
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"git.tornberg.me/go-cart-actor/pkg/discovery"
|
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
)
|
)
|
||||||
@@ -24,10 +23,14 @@ 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 discovery.DiscoveryTarget) {
|
||||||
|
|
||||||
go func(hw discovery.Discovery) {
|
go func(hw discovery.Discovery) {
|
||||||
if hw == nil {
|
if hw == nil {
|
||||||
|
|||||||
+104
-79
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
@@ -13,13 +12,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.tornberg.me/go-cart-actor/pkg/actor"
|
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||||
"git.tornberg.me/go-cart-actor/pkg/cart"
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
"git.k6n.net/go-cart-actor/pkg/promotions"
|
||||||
"git.tornberg.me/go-cart-actor/pkg/promotions"
|
"git.k6n.net/go-cart-actor/pkg/proxy"
|
||||||
"git.tornberg.me/go-cart-actor/pkg/proxy"
|
"git.k6n.net/go-cart-actor/pkg/voucher"
|
||||||
"git.tornberg.me/go-cart-actor/pkg/voucher"
|
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
||||||
"git.tornberg.me/mats/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"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
@@ -39,9 +37,8 @@ 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")
|
||||||
@@ -69,6 +66,21 @@ type CartChangeEvent struct {
|
|||||||
Mutations []actor.ApplyResult `json:"mutations"`
|
Mutations []actor.ApplyResult `json:"mutations"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func matchesSkuAndLocation(update inventory.InventoryResult, item cart.CartItem) bool {
|
||||||
|
if string(update.SKU) == item.Sku {
|
||||||
|
if update.LocationID == "se" && item.StoreId == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if item.StoreId == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if *item.StoreId == string(update.LocationID) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
controlPlaneConfig := actor.DefaultServerConfig()
|
controlPlaneConfig := actor.DefaultServerConfig()
|
||||||
@@ -80,21 +92,39 @@ func main() {
|
|||||||
|
|
||||||
log.Printf("loaded %d promotions", len(promotionData.State.Promotions))
|
log.Printf("loaded %d promotions", len(promotionData.State.Promotions))
|
||||||
|
|
||||||
promotionService := promotions.NewPromotionService(nil)
|
inventoryPubSub := actor.NewPubSub[inventory.InventoryChange]()
|
||||||
|
|
||||||
reg := cart.NewCartMultationRegistry()
|
// promotionService := promotions.NewPromotionService(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
inventoryReservationService, err := inventory.NewRedisCartReservationService(rdb)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error creating inventory reservation service: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reg := cart.NewCartMultationRegistry(cart.NewCartMutationContext(inventoryReservationService))
|
||||||
reg.RegisterProcessor(
|
reg.RegisterProcessor(
|
||||||
actor.NewMutationProcessor(func(ctx context.Context, g *cart.CartGrain) error {
|
actor.NewMutationProcessor(func(ctx context.Context, g *cart.CartGrain) error {
|
||||||
_, span := tracer.Start(ctx, "Totals and promotions")
|
_, span := tracer.Start(ctx, "Totals and promotions")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
g.UpdateTotals()
|
g.UpdateTotals()
|
||||||
|
g.Version++
|
||||||
|
// promotionCtx := promotions.NewContextFromCart(g, promotions.WithNow(time.Now()), promotions.WithCustomerSegment("vip"))
|
||||||
|
// _, actions := promotionService.EvaluateAll(promotionData.State.Promotions, promotionCtx)
|
||||||
|
// for _, action := range actions {
|
||||||
|
|
||||||
promotionCtx := promotions.NewContextFromCart(g, promotions.WithNow(time.Now()), promotions.WithCustomerSegment("vip"))
|
// log.Printf("apply: %+v", action)
|
||||||
_, actions := promotionService.EvaluateAll(promotionData.State.Promotions, promotionCtx)
|
|
||||||
for _, action := range actions {
|
// g.UpdateTotals()
|
||||||
log.Printf("apply: %+v", action)
|
// }
|
||||||
g.UpdateTotals()
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -110,14 +140,48 @@ func main() {
|
|||||||
ret := cart.NewCartGrain(id, time.Now())
|
ret := cart.NewCartGrain(id, time.Now())
|
||||||
// Set baseline lastChange at spawn; replay may update it to last event timestamp.
|
// Set baseline lastChange at spawn; replay may update it to last event timestamp.
|
||||||
|
|
||||||
|
inventoryPubSub.Subscribe(ret.HandleInventoryChange)
|
||||||
err := diskStorage.LoadEvents(ctx, id, ret)
|
err := diskStorage.LoadEvents(ctx, id, ret)
|
||||||
|
if err == nil && inventoryService != nil {
|
||||||
|
refs := make([]*inventory.InventoryReference, 0)
|
||||||
|
for _, item := range ret.Items {
|
||||||
|
refs = append(refs, &inventory.InventoryReference{
|
||||||
|
SKU: inventory.SKU(item.Sku),
|
||||||
|
LocationID: getLocationId(item),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_, span := tracer.Start(ctx, "update inventory")
|
||||||
|
defer span.End()
|
||||||
|
res, err := inventoryService.GetInventoryBatch(ctx, refs...)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("unable to update inventory %v", err)
|
||||||
|
} else {
|
||||||
|
for _, update := range res {
|
||||||
|
for _, item := range ret.Items {
|
||||||
|
if matchesSkuAndLocation(update, *item) && update.Quantity != uint32(item.Stock) {
|
||||||
|
// maybe apply an update to give visibility to the cart
|
||||||
|
item.Stock = uint16(update.Quantity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ret, err
|
return ret, err
|
||||||
},
|
},
|
||||||
SpawnHost: func(host string) (actor.Host, error) {
|
Destroy: func(grain actor.Grain[cart.CartGrain]) error {
|
||||||
return proxy.NewRemoteHost(host)
|
cart, err := grain.GetCurrentState()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
inventoryPubSub.Unsubscribe(cart.HandleInventoryChange)
|
||||||
|
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
TTL: 15 * time.Minute,
|
SpawnHost: func(host string) (actor.Host[cart.CartGrain], error) {
|
||||||
|
return proxy.NewRemoteHost[cart.CartGrain](host)
|
||||||
|
},
|
||||||
|
TTL: 5 * time.Minute,
|
||||||
PoolSize: 2 * 65535,
|
PoolSize: 2 * 65535,
|
||||||
Hostname: podIp,
|
Hostname: podIp,
|
||||||
}
|
}
|
||||||
@@ -127,42 +191,17 @@ func main() {
|
|||||||
log.Fatalf("Error creating cart pool: %v\n", err)
|
log.Fatalf("Error creating cart pool: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pool.SetPubSub(actor.NewPubSub(func(id uint64, event actor.Event) {
|
syncedServer := NewPoolServer(pool, fmt.Sprintf("%s, %s", name, podIp), inventoryService, inventoryReservationService)
|
||||||
grain, _ := pool.Get(context.Background(), id)
|
|
||||||
if sub, ok := any(grain).(actor.Subscribable); ok {
|
|
||||||
sub.Notify(event)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
klarnaClient := NewKlarnaClient(KlarnaPlaygroundUrl, os.Getenv("KLARNA_API_USERNAME"), os.Getenv("KLARNA_API_PASSWORD"))
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
syncedServer := NewPoolServer(pool, fmt.Sprintf("%s, %s", name, podIp), klarnaClient, inventoryService)
|
|
||||||
|
|
||||||
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 == "" {
|
grpcSrv, err := actor.NewControlServer[cart.CartGrain](controlPlaneConfig, pool)
|
||||||
log.Printf("no connection to amqp defined")
|
|
||||||
} else {
|
|
||||||
app.HandleCheckoutRequests(amqpUrl, mux, inventoryService)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -245,6 +284,20 @@ func main() {
|
|||||||
srvErr <- srv.ListenAndServe()
|
srvErr <- srv.ListenAndServe()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
listener := inventory.NewInventoryChangeListener(rdb, context.Background(), func(changes []inventory.InventoryChange) {
|
||||||
|
for _, change := range changes {
|
||||||
|
log.Printf("inventory change: %v", change)
|
||||||
|
inventoryPubSub.Publish(change)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := listener.Start()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Unable to start inventory listener: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
log.Print("Server started at port 8080")
|
log.Print("Server started at port 8080")
|
||||||
|
|
||||||
go http.ListenAndServe(":8081", debugMux)
|
go http.ListenAndServe(":8081", debugMux)
|
||||||
@@ -260,31 +313,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
|
|
||||||
}
|
|
||||||
|
|||||||
+60
-297
@@ -1,26 +1,26 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.tornberg.me/go-cart-actor/pkg/actor"
|
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||||
"git.tornberg.me/go-cart-actor/pkg/cart"
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||||
"git.tornberg.me/go-cart-actor/pkg/voucher"
|
|
||||||
"git.tornberg.me/mats/go-redis-inventory/pkg/inventory"
|
"git.k6n.net/go-cart-actor/pkg/voucher"
|
||||||
"github.com/gogo/protobuf/proto"
|
"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"
|
||||||
"go.opentelemetry.io/contrib/bridges/otelslog"
|
"go.opentelemetry.io/contrib/bridges/otelslog"
|
||||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
"google.golang.org/protobuf/types/known/anypb"
|
"google.golang.org/protobuf/types/known/anypb"
|
||||||
|
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
@@ -41,22 +41,24 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PoolServer struct {
|
type PoolServer struct {
|
||||||
actor.GrainPool[*cart.CartGrain]
|
actor.GrainPool[cart.CartGrain]
|
||||||
pod_name string
|
pod_name string
|
||||||
klarnaClient *KlarnaClient
|
inventoryService inventory.InventoryService
|
||||||
inventoryService inventory.InventoryService
|
reservationService inventory.CartReservationService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPoolServer(pool actor.GrainPool[*cart.CartGrain], pod_name string, klarnaClient *KlarnaClient, inventoryService inventory.InventoryService) *PoolServer {
|
func NewPoolServer(pool actor.GrainPool[cart.CartGrain], pod_name string, inventoryService inventory.InventoryService, inventoryReservationService inventory.CartReservationService) *PoolServer {
|
||||||
return &PoolServer{
|
srv := &PoolServer{
|
||||||
GrainPool: pool,
|
GrainPool: pool,
|
||||||
pod_name: pod_name,
|
pod_name: pod_name,
|
||||||
klarnaClient: klarnaClient,
|
inventoryService: inventoryService,
|
||||||
inventoryService: inventoryService,
|
reservationService: inventoryReservationService,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return srv
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PoolServer) ApplyLocal(ctx context.Context, id cart.CartId, mutation ...proto.Message) (*actor.MutationResult[*cart.CartGrain], error) {
|
func (s *PoolServer) ApplyLocal(ctx context.Context, id cart.CartId, mutation ...proto.Message) (*actor.MutationResult[cart.CartGrain], error) {
|
||||||
return s.Apply(ctx, uint64(id), mutation...)
|
return s.Apply(ctx, uint64(id), mutation...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +77,7 @@ func (s *PoolServer) AddSkuToCartHandler(w http.ResponseWriter, r *http.Request,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := s.ApplyLocal(r.Context(), id, msg)
|
data, err := s.ApplyLocal(r.Context(), id, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -114,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)
|
||||||
@@ -249,7 +187,9 @@ func (s *PoolServer) AddMultipleItemHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
reply, err := s.ApplyLocal(r.Context(), id, getMultipleAddMessages(r.Context(), setCartItems.Items, setCartItems.Country)...)
|
msgs := getMultipleAddMessages(r.Context(), setCartItems.Items, setCartItems.Country)
|
||||||
|
|
||||||
|
reply, err := s.ApplyLocal(r.Context(), id, msgs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -273,6 +213,7 @@ func (s *PoolServer) AddSkuRequestHandler(w http.ResponseWriter, r *http.Request
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
reply, err := s.ApplyLocal(r.Context(), id, msg)
|
reply, err := s.ApplyLocal(r.Context(), id, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -300,98 +241,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{
|
|
||||||
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 (s *PoolServer) CreateOrUpdateCheckout(r *http.Request, id cart.CartId) (*CheckoutOrder, error) {
|
|
||||||
host := getOriginalHost(r)
|
|
||||||
country := getCountryFromHost(host)
|
|
||||||
meta := &CheckoutMeta{
|
|
||||||
Terms: fmt.Sprintf("https://%s/terms", host),
|
|
||||||
Checkout: fmt.Sprintf("https://%s/checkout?order_id={checkout.order.id}", host),
|
|
||||||
Confirmation: fmt.Sprintf("https://%s/confirmation/{checkout.order.id}", host),
|
|
||||||
Country: country,
|
|
||||||
Currency: getCurrency(country),
|
|
||||||
Locale: getLocale(country),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
||||||
@@ -405,51 +254,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 {
|
||||||
@@ -467,34 +271,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 {
|
||||||
@@ -505,7 +281,7 @@ func (s *PoolServer) ProxyHandler(fn func(w http.ResponseWriter, r *http.Request
|
|||||||
span.SetAttributes(hostAttr)
|
span.SetAttributes(hostAttr)
|
||||||
logger.InfoContext(ctx, "cart proxyed", "result", ownerHost.Name())
|
logger.InfoContext(ctx, "cart proxyed", "result", ownerHost.Name())
|
||||||
proxyCalls.Add(ctx, 1, metric.WithAttributes(hostAttr))
|
proxyCalls.Add(ctx, 1, metric.WithAttributes(hostAttr))
|
||||||
handled, err := ownerHost.Proxy(uint64(cartId), w, r)
|
handled, err := ownerHost.Proxy(uint64(cartId), w, r, nil)
|
||||||
|
|
||||||
grainLookups.Inc()
|
grainLookups.Inc()
|
||||||
if err == nil && handled {
|
if err == nil && handled {
|
||||||
@@ -521,13 +297,10 @@ func (s *PoolServer) ProxyHandler(fn func(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
tracer = otel.Tracer(name)
|
tracer = otel.Tracer(name)
|
||||||
|
|
||||||
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() {
|
||||||
@@ -551,10 +324,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
|
||||||
@@ -603,26 +372,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")
|
||||||
@@ -687,19 +436,45 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *PoolServer) GetAnywhere(ctx context.Context, cartId cart.CartId) (*cart.CartGrain, error) {
|
||||||
|
id := uint64(cartId)
|
||||||
|
if host, isOnOtherHost := s.OwnerHost(id); isOnOtherHost {
|
||||||
|
return host.Get(ctx, id)
|
||||||
|
}
|
||||||
|
return s.Get(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PoolServer) ApplyAnywhere(ctx context.Context, cartId cart.CartId, msgs ...proto.Message) error {
|
||||||
|
id := uint64(cartId)
|
||||||
|
if host, isOnOtherHost := s.OwnerHost(id); isOnOtherHost {
|
||||||
|
_, err := host.Apply(ctx, id, msgs...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := s.Apply(ctx, id, msgs...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -731,37 +506,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("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)))
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.tornberg.me/go-cart-actor/pkg/cart"
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
messages "git.tornberg.me/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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
|
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||||
|
adyenCheckout "github.com/adyen/adyen-go-api-library/v21/src/checkout"
|
||||||
|
"github.com/adyen/adyen-go-api-library/v21/src/common"
|
||||||
|
"github.com/adyen/adyen-go-api-library/v21/src/hmacvalidator"
|
||||||
|
"github.com/adyen/adyen-go-api-library/v21/src/webhook"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"google.golang.org/protobuf/types/known/anypb"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionRequest struct {
|
||||||
|
SessionId *string `json:"sessionId,omitempty"`
|
||||||
|
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 {
|
||||||
|
// service := s.adyenClient.Checkout()
|
||||||
|
// req := service.PaymentsApi.GetResultOfPaymentSessionInput(pa).SessionResult(payload.SessionResult)
|
||||||
|
// res, _, err := service.PaymentsApi.GetResultOfPaymentSession(r.Context(), req)
|
||||||
|
// 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) (*cart.CartId, error) {
|
||||||
|
cartId, ok := cart.ParseCartId(item.MerchantReference)
|
||||||
|
if !ok {
|
||||||
|
log.Printf("The notification does not have a valid cartId: %s", item.MerchantReference)
|
||||||
|
return nil, errors.New("invalid cart id")
|
||||||
|
}
|
||||||
|
return &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
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, notificationItem := range *notificationRequest.NotificationItems {
|
||||||
|
item := notificationItem.NotificationRequestItem
|
||||||
|
log.Printf("Recieved notification event code: %s, %+v", item.EventCode, item)
|
||||||
|
|
||||||
|
isValid := hmacvalidator.ValidateHmac(item, hmacKey)
|
||||||
|
if !isValid {
|
||||||
|
log.Printf("notification hmac not valid %s, %v", item.EventCode, item)
|
||||||
|
http.Error(w, "Invalid HMAC", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
// Marshal item data for PaymentEvent
|
||||||
|
dataBytes, err := json.Marshal(item)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error marshaling item: %v", err)
|
||||||
|
http.Error(w, "Error marshaling item", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch item.EventCode {
|
||||||
|
case "CAPTURE":
|
||||||
|
checkoutId, err := getCheckoutIdFromNotificationItem(item)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not get checkout id: %v", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Capture status: %v", item.Success)
|
||||||
|
isSuccess := item.Success == "true"
|
||||||
|
|
||||||
|
// If successful, apply payment completed
|
||||||
|
//if isSuccess {
|
||||||
|
if err := s.ApplyAnywhere(r.Context(), *checkoutId,
|
||||||
|
&messages.PaymentEvent{
|
||||||
|
PaymentId: item.PspReference,
|
||||||
|
Success: isSuccess,
|
||||||
|
Name: item.EventCode,
|
||||||
|
Data: &anypb.Any{Value: dataBytes},
|
||||||
|
}, &messages.PaymentCompleted{
|
||||||
|
PaymentId: item.PspReference,
|
||||||
|
Status: item.Success,
|
||||||
|
Amount: item.Amount.Value,
|
||||||
|
Currency: item.Amount.Currency,
|
||||||
|
ProcessorReference: &item.PspReference,
|
||||||
|
CompletedAt: timestamppb.New(time.Now()),
|
||||||
|
}); err != nil {
|
||||||
|
http.Error(w, "Message not parsed", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//}
|
||||||
|
|
||||||
|
case "AUTHORISATION":
|
||||||
|
|
||||||
|
isSuccess := item.Success == "true"
|
||||||
|
log.Printf("Handling auth: %+v", item)
|
||||||
|
checkoutId, err := getCheckoutIdFromNotificationItem(item)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not get checkout id: %v", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msgs := []proto.Message{
|
||||||
|
&messages.PaymentEvent{
|
||||||
|
PaymentId: item.PspReference,
|
||||||
|
Success: isSuccess,
|
||||||
|
Name: item.EventCode,
|
||||||
|
Data: &anypb.Any{Value: dataBytes},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if isSuccess {
|
||||||
|
msgs = append(msgs, &messages.PaymentCompleted{
|
||||||
|
PaymentId: item.PspReference,
|
||||||
|
Status: item.Success,
|
||||||
|
Amount: item.Amount.Value,
|
||||||
|
CompletedAt: timestamppb.Now(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
msgs = append(msgs, &messages.PaymentDeclined{
|
||||||
|
PaymentId: item.PspReference,
|
||||||
|
Message: item.Reason,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := s.ApplyAnywhere(r.Context(), *checkoutId, msgs...); err != nil {
|
||||||
|
log.Printf("error applying authorization event: %v", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If successful authorization, trigger capture
|
||||||
|
if isSuccess {
|
||||||
|
|
||||||
|
pspReference := item.PspReference
|
||||||
|
uid := uuid.New().String()
|
||||||
|
ref := checkoutId.String()
|
||||||
|
req := service.ModificationsApi.CaptureAuthorisedPaymentInput(pspReference).IdempotencyKey(uid).PaymentCaptureRequest(adyenCheckout.PaymentCaptureRequest{
|
||||||
|
Amount: adyenCheckout.Amount(item.Amount),
|
||||||
|
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.ApplyAnywhere(r.Context(), *checkoutId, &messages.OrderCreated{
|
||||||
|
OrderId: res.PaymentPspReference,
|
||||||
|
Status: item.EventCode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.Printf("Unknown event code: %s", item.EventCode)
|
||||||
|
log.Printf("Item data: %+v", item)
|
||||||
|
isSuccess := item.Success == "true"
|
||||||
|
checkoutId, err := getCheckoutIdFromNotificationItem(item)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not get checkout id: %v", err)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if err := s.ApplyAnywhere(r.Context(), *checkoutId, &messages.PaymentEvent{
|
||||||
|
PaymentId: item.PspReference,
|
||||||
|
Success: isSuccess,
|
||||||
|
Name: item.EventCode,
|
||||||
|
Data: &anypb.Any{Value: dataBytes},
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("error applying payment event: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"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/%s", 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 {
|
||||||
|
log.Printf("request to %s failed with status %s", url, resp.Status)
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestGetCartGrain_RealService tests against the actual service at https://cart.k6n.net/
|
||||||
|
// This test is skipped by default and can be run with: go test -run TestGetCartGrain_RealService
|
||||||
|
func TestGetCartGrain_RealService(t *testing.T) {
|
||||||
|
t.Skip("Skipping integration test against real service")
|
||||||
|
|
||||||
|
client := NewCartClient("https://cart.k6n.net")
|
||||||
|
|
||||||
|
// You would need a real cart ID that exists in the system
|
||||||
|
// For example: cartId := cart.NewCartId(123, 456)
|
||||||
|
cartId := cart.MustParseCartId("JkfG6bRNLMy")
|
||||||
|
|
||||||
|
grain, err := client.getCartGrain(context.Background(), cartId)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get cart grain: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if grain == nil {
|
||||||
|
t.Fatal("Expected grain to be non-nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Successfully retrieved cart grain: %+v", grain)
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckoutMeta carries the external / URL metadata required to build a
|
||||||
|
// Klarna CheckoutOrder from a CartGrain snapshot. It deliberately excludes
|
||||||
|
// any Klarna-specific response fields (HTML snippet, client token, etc.).
|
||||||
|
type CheckoutMeta struct {
|
||||||
|
SiteUrl string
|
||||||
|
// Terms string
|
||||||
|
// Checkout string
|
||||||
|
// Confirmation string
|
||||||
|
ClientIp string
|
||||||
|
Country string
|
||||||
|
Currency string // optional override (defaults to "SEK" if empty)
|
||||||
|
Locale string // optional override (defaults to "sv-se" if empty)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildCheckoutOrderPayload converts the current cart grain + meta information
|
||||||
|
// into a CheckoutOrder domain struct and returns its JSON-serialized payload
|
||||||
|
// (to send to Klarna) alongside the structured CheckoutOrder object.
|
||||||
|
//
|
||||||
|
// This function is PURE: it does not perform any network I/O or mutate the
|
||||||
|
// grain. The caller is responsible for:
|
||||||
|
//
|
||||||
|
// 1. Choosing whether to create or update the Klarna order.
|
||||||
|
// 2. Invoking KlarnaClient.CreateOrder / UpdateOrder with the returned payload.
|
||||||
|
// 3. Applying an InitializeCheckout mutation (or equivalent) with the
|
||||||
|
// resulting Klarna order id + status.
|
||||||
|
//
|
||||||
|
// If you later need to support different tax rates per line, you can extend
|
||||||
|
// CartItem / Delivery to expose that data and propagate it here.
|
||||||
|
func BuildCheckoutOrderPayload(grain *checkout.CheckoutGrain, meta *CheckoutMeta) ([]byte, *CheckoutOrder, error) {
|
||||||
|
if grain == nil {
|
||||||
|
return nil, nil, fmt.Errorf("nil grain")
|
||||||
|
}
|
||||||
|
if meta == nil {
|
||||||
|
return nil, nil, fmt.Errorf("nil checkout meta")
|
||||||
|
}
|
||||||
|
|
||||||
|
currency := meta.Currency
|
||||||
|
if currency == "" {
|
||||||
|
currency = "SEK"
|
||||||
|
}
|
||||||
|
locale := meta.Locale
|
||||||
|
if locale == "" {
|
||||||
|
locale = "sv-se"
|
||||||
|
}
|
||||||
|
country := meta.Country
|
||||||
|
if country == "" {
|
||||||
|
country = "SE" // sensible default; adjust if multi-country support changes
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := make([]*Line, 0, len(grain.CartState.Items)+len(grain.Deliveries))
|
||||||
|
|
||||||
|
// Item lines
|
||||||
|
for _, it := range grain.CartState.Items {
|
||||||
|
if it == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lines = append(lines, &Line{
|
||||||
|
Type: "physical",
|
||||||
|
Reference: it.Sku,
|
||||||
|
Name: it.Meta.Name,
|
||||||
|
Quantity: int(it.Quantity),
|
||||||
|
UnitPrice: int(it.Price.IncVat),
|
||||||
|
TaxRate: it.Tax, // TODO: derive if variable tax rates are introduced
|
||||||
|
QuantityUnit: "st",
|
||||||
|
TotalAmount: int(it.TotalPrice.IncVat),
|
||||||
|
TotalTaxAmount: int(it.TotalPrice.TotalVat()),
|
||||||
|
ImageURL: fmt.Sprintf("https://www.elgiganten.se%s", it.Meta.Image),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
total := cart.NewPrice()
|
||||||
|
total.Add(*grain.CartState.TotalPrice)
|
||||||
|
|
||||||
|
// Delivery lines
|
||||||
|
for _, d := range grain.Deliveries {
|
||||||
|
if d == nil || d.Price.IncVat <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
//total.Add(d.Price)
|
||||||
|
lines = append(lines, &Line{
|
||||||
|
Type: "shipping_fee",
|
||||||
|
Reference: d.Provider,
|
||||||
|
Name: "Delivery",
|
||||||
|
Quantity: 1,
|
||||||
|
UnitPrice: int(d.Price.IncVat),
|
||||||
|
TaxRate: 2500,
|
||||||
|
QuantityUnit: "st",
|
||||||
|
TotalAmount: int(d.Price.IncVat),
|
||||||
|
TotalTaxAmount: int(d.Price.TotalVat()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
order := &CheckoutOrder{
|
||||||
|
PurchaseCountry: country,
|
||||||
|
PurchaseCurrency: currency,
|
||||||
|
Locale: locale,
|
||||||
|
OrderAmount: int(total.IncVat),
|
||||||
|
OrderTaxAmount: int(total.TotalVat()),
|
||||||
|
OrderLines: lines,
|
||||||
|
MerchantReference1: grain.Id.String(),
|
||||||
|
MerchantURLS: &CheckoutMerchantURLS{
|
||||||
|
Terms: fmt.Sprintf("%s/terms", meta.SiteUrl),
|
||||||
|
Checkout: fmt.Sprintf("%s/checkout?order_id={checkout.order.id}&provider=klarna", meta.SiteUrl),
|
||||||
|
Confirmation: fmt.Sprintf("%s/checkout?order_id={checkout.order.id}&provider=klarna", meta.SiteUrl),
|
||||||
|
Notification: "https://cart.k6n.net/payment/klarna/notification",
|
||||||
|
Validation: "https://cart.k6n.net/payment/klarna/validate",
|
||||||
|
Push: "https://cart.k6n.net/payment/klarna/push?order_id={checkout.order.id}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := json.Marshal(order)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("marshal checkout order: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload, order, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCheckoutMetaFromRequest(r *http.Request) *CheckoutMeta {
|
||||||
|
host := getOriginalHost(r)
|
||||||
|
country := getCountryFromHost(host)
|
||||||
|
return &CheckoutMeta{
|
||||||
|
ClientIp: getClientIp(r),
|
||||||
|
SiteUrl: fmt.Sprintf("https://%s", host),
|
||||||
|
Country: country,
|
||||||
|
Currency: getCurrency(country),
|
||||||
|
Locale: getLocale(country),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildAdyenCheckoutSession(grain *checkout.CheckoutGrain, meta *CheckoutMeta) (*adyenCheckout.CreateCheckoutSessionRequest, error) {
|
||||||
|
if grain == nil {
|
||||||
|
return nil, fmt.Errorf("nil grain")
|
||||||
|
}
|
||||||
|
if meta == nil {
|
||||||
|
return nil, fmt.Errorf("nil checkout meta")
|
||||||
|
}
|
||||||
|
|
||||||
|
currency := meta.Currency
|
||||||
|
if currency == "" {
|
||||||
|
currency = "SEK"
|
||||||
|
}
|
||||||
|
country := meta.Country
|
||||||
|
if country == "" {
|
||||||
|
country = "SE"
|
||||||
|
}
|
||||||
|
|
||||||
|
lineItems := make([]adyenCheckout.LineItem, 0, len(grain.CartState.Items)+len(grain.Deliveries))
|
||||||
|
|
||||||
|
// Item lines
|
||||||
|
for _, it := range grain.CartState.Items {
|
||||||
|
if it == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lineItems = append(lineItems, adyenCheckout.LineItem{
|
||||||
|
Quantity: common.PtrInt64(int64(it.Quantity)),
|
||||||
|
AmountIncludingTax: common.PtrInt64(it.TotalPrice.IncVat),
|
||||||
|
Description: common.PtrString(it.Meta.Name),
|
||||||
|
AmountExcludingTax: common.PtrInt64(it.TotalPrice.ValueExVat()),
|
||||||
|
TaxAmount: common.PtrInt64(it.TotalPrice.TotalVat()),
|
||||||
|
TaxPercentage: common.PtrInt64(int64(it.Tax)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
total := cart.NewPrice()
|
||||||
|
total.Add(*grain.CartState.TotalPrice)
|
||||||
|
|
||||||
|
// Delivery lines
|
||||||
|
for _, d := range grain.Deliveries {
|
||||||
|
if d == nil || d.Price.IncVat <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lineItems = append(lineItems, adyenCheckout.LineItem{
|
||||||
|
Quantity: common.PtrInt64(1),
|
||||||
|
AmountIncludingTax: common.PtrInt64(d.Price.IncVat),
|
||||||
|
Description: common.PtrString("Delivery"),
|
||||||
|
AmountExcludingTax: common.PtrInt64(d.Price.ValueExVat()),
|
||||||
|
TaxPercentage: common.PtrInt64(25),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &adyenCheckout.CreateCheckoutSessionRequest{
|
||||||
|
Reference: grain.Id.String(),
|
||||||
|
Amount: adyenCheckout.Amount{
|
||||||
|
Value: total.IncVat,
|
||||||
|
Currency: currency,
|
||||||
|
},
|
||||||
|
CountryCode: common.PtrString(country),
|
||||||
|
MerchantAccount: "ElgigantenECOM",
|
||||||
|
Channel: common.PtrString("Web"),
|
||||||
|
ShopperIP: common.PtrString(meta.ClientIp),
|
||||||
|
ReturnUrl: fmt.Sprintf("%s/payment/adyen/return", meta.SiteUrl),
|
||||||
|
LineItems: lineItems,
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"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 discovery.DiscoveryTarget) {
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
@@ -80,7 +80,7 @@ func (k *KlarnaClient) CreateOrder(ctx context.Context, reader io.Reader) (*Chec
|
|||||||
req.Header.Add("Content-Type", "application/json")
|
req.Header.Add("Content-Type", "application/json")
|
||||||
req.SetBasicAuth(k.UserName, k.Password)
|
req.SetBasicAuth(k.UserName, k.Password)
|
||||||
|
|
||||||
res, err := http.DefaultClient.Do(req)
|
res, err := k.client.Do(req)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ func (k *KlarnaClient) UpdateOrder(ctx context.Context, orderId string, reader i
|
|||||||
req.Header.Add("Content-Type", "application/json")
|
req.Header.Add("Content-Type", "application/json")
|
||||||
req.SetBasicAuth(k.UserName, k.Password)
|
req.SetBasicAuth(k.UserName, k.Password)
|
||||||
|
|
||||||
res, err := http.DefaultClient.Do(req)
|
res, err := k.client.Do(req)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -119,7 +119,7 @@ func (k *KlarnaClient) AbortOrder(ctx context.Context, orderId string) error {
|
|||||||
req = req.WithContext(spanCtx)
|
req = req.WithContext(spanCtx)
|
||||||
req.SetBasicAuth(k.UserName, k.Password)
|
req.SetBasicAuth(k.UserName, k.Password)
|
||||||
|
|
||||||
_, err = http.DefaultClient.Do(req)
|
_, err = k.client.Do(req)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +137,6 @@ func (k *KlarnaClient) AcknowledgeOrder(ctx context.Context, orderId string) err
|
|||||||
req.SetBasicAuth(k.UserName, k.Password)
|
req.SetBasicAuth(k.UserName, k.Password)
|
||||||
req.Header.Add("Klarna-Idempotency-Key", id.String())
|
req.Header.Add("Klarna-Idempotency-Key", id.String())
|
||||||
|
|
||||||
_, err = http.DefaultClient.Do(req)
|
_, err = k.client.Do(req)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
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"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* 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) KlarnaSessionHandler(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", "application/json; charset=utf-8")
|
||||||
|
// return json.NewEncoder(w).Encode(order)
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
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)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
s.ApplyAnywhere(r.Context(), grain.Id, &messages.PaymentCompleted{
|
||||||
|
PaymentId: orderId,
|
||||||
|
Status: "completed",
|
||||||
|
ProcessorReference: &order.ID,
|
||||||
|
Amount: int64(order.OrderAmount),
|
||||||
|
Currency: order.PurchaseCurrency,
|
||||||
|
CompletedAt: timestamppb.Now(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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.GetAnywhere(ctx, cartId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get cart grain: %w", err)
|
||||||
|
}
|
||||||
|
return grain, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
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
|
||||||
|
// Load persisted events/state for this checkout if present
|
||||||
|
if err := diskStorage.LoadEvents(ctx, id, ret); err != nil {
|
||||||
|
// Return the grain along with error (e.g., not found) so callers can decide
|
||||||
|
return ret, err
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
},
|
||||||
|
Destroy: func(grain actor.Grain[checkout.CheckoutGrain]) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
SpawnHost: func(host string) (actor.Host[checkout.CheckoutGrain], 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,543 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"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/anypb"
|
||||||
|
"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) SetDeliveryHandler(w http.ResponseWriter, r *http.Request, id checkout.CheckoutId) error {
|
||||||
|
var msg messages.SetDelivery
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.ApplyLocal(r.Context(), id, &msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.WriteResult(w, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) RemoveDeliveryHandler(w http.ResponseWriter, r *http.Request, id checkout.CheckoutId) error {
|
||||||
|
deliveryId := r.PathValue("id")
|
||||||
|
uintDeliveryId, err := strconv.ParseUint(deliveryId, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
msg := &messages.RemoveDelivery{
|
||||||
|
Id: uint32(uintDeliveryId),
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.ApplyLocal(r.Context(), id, msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.WriteResult(w, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) SetPickupPointHandler(w http.ResponseWriter, r *http.Request, id checkout.CheckoutId) error {
|
||||||
|
var msg messages.SetPickupPoint
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.ApplyLocal(r.Context(), id, &msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.WriteResult(w, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) InitializeCheckoutHandler(w http.ResponseWriter, r *http.Request, id checkout.CheckoutId) error {
|
||||||
|
var msg messages.InitializeCheckout
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.ApplyLocal(r.Context(), id, &msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.WriteResult(w, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) InventoryReservedHandler(w http.ResponseWriter, r *http.Request, id checkout.CheckoutId) error {
|
||||||
|
var msg messages.InventoryReserved
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.ApplyLocal(r.Context(), id, &msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.WriteResult(w, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) OrderCreatedHandler(w http.ResponseWriter, r *http.Request, id checkout.CheckoutId) error {
|
||||||
|
var msg messages.OrderCreated
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.ApplyLocal(r.Context(), id, &msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.WriteResult(w, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) ConfirmationViewedHandler(w http.ResponseWriter, r *http.Request, id checkout.CheckoutId) error {
|
||||||
|
var msg messages.ConfirmationViewed
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.ApplyLocal(r.Context(), id, &msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.WriteResult(w, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) ContactDetailsUpdatedHandler(w http.ResponseWriter, r *http.Request, id checkout.CheckoutId) error {
|
||||||
|
var msg messages.ContactDetailsUpdated
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.ApplyLocal(r.Context(), id, &msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.WriteResult(w, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) StartCheckoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cartIdStr := r.PathValue("cartid")
|
||||||
|
if cartIdStr == "" {
|
||||||
|
http.Error(w, "cart id required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cartId, ok := cart.ParseCartId(cartIdStr)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "invalid cart id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch cart state from cart service
|
||||||
|
cartGrain, err := s.cartClient.getCartGrain(r.Context(), cartId)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to fetch cart", "error", err, "cartId", cartId)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize cart state to Any
|
||||||
|
cartStateBytes, err := json.Marshal(cartGrain)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to marshal cart state", "error", err)
|
||||||
|
http.Error(w, "failed to process cart state", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create checkout with same ID as cart
|
||||||
|
var checkoutId checkout.CheckoutId = cart.MustNewCartId()
|
||||||
|
cookie, err := r.Cookie(checkoutCookieName)
|
||||||
|
if err == nil {
|
||||||
|
parsed, ok := cart.ParseCartId(cookie.Value)
|
||||||
|
if ok {
|
||||||
|
checkoutId = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize checkout with cart state wrapped in Any
|
||||||
|
cartStateAny := &messages.InitializeCheckout{
|
||||||
|
OrderId: "",
|
||||||
|
CartId: uint64(cartId),
|
||||||
|
Version: uint32(cartGrain.Version),
|
||||||
|
CartState: &anypb.Any{
|
||||||
|
TypeUrl: "type.googleapis.com/cart.CartGrain",
|
||||||
|
Value: cartStateBytes,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.ApplyLocal(r.Context(), checkoutId, cartStateAny)
|
||||||
|
if err != nil {
|
||||||
|
setCheckoutCookie(w, 0, r.TLS != nil)
|
||||||
|
logger.Error("failed to initialize checkout", "error", err)
|
||||||
|
http.Error(w, "failed to initialize checkout", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set checkout cookie
|
||||||
|
setCheckoutCookie(w, checkoutId, r.TLS != nil)
|
||||||
|
|
||||||
|
if err := s.WriteResult(w, &result.Result); err != nil {
|
||||||
|
logger.Error("failed to write result", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, grain *checkout.CheckoutGrain, orderId *string) (*CheckoutOrder, error) {
|
||||||
|
|
||||||
|
meta := GetCheckoutMetaFromRequest(r)
|
||||||
|
|
||||||
|
payload, _, err := BuildCheckoutOrderPayload(grain, meta)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var payment *checkout.Payment
|
||||||
|
if orderId != nil {
|
||||||
|
payment, _ = grain.FindPayment(*orderId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if payment != nil && payment.PaymentId != "" {
|
||||||
|
return s.klarnaClient.UpdateOrder(r.Context(), payment.PaymentId, 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()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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) GetAnywhere(ctx context.Context, checkoutId cart.CartId) (*checkout.CheckoutGrain, error) {
|
||||||
|
id := uint64(checkoutId)
|
||||||
|
if host, isOnOtherHost := s.OwnerHost(id); isOnOtherHost {
|
||||||
|
return host.Get(ctx, id)
|
||||||
|
}
|
||||||
|
return s.Get(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) ApplyAnywhere(ctx context.Context, checkoutId cart.CartId, msgs ...proto.Message) error {
|
||||||
|
id := uint64(checkoutId)
|
||||||
|
if host, isOnOtherHost := s.OwnerHost(id); isOnOtherHost {
|
||||||
|
_, err := host.Apply(ctx, id, msgs...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := s.Apply(ctx, id, msgs...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type StartPayment struct {
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Method string `json:"method,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) GetPaymentSessionHandler(w http.ResponseWriter, r *http.Request, checkoutId checkout.CheckoutId) error {
|
||||||
|
paymentId := r.PathValue("id")
|
||||||
|
grain, err := s.Get(r.Context(), uint64(checkoutId))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
payment, ok := grain.FindPayment(paymentId)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "payment not found", http.StatusNotFound)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch payment.Provider {
|
||||||
|
case "adyen":
|
||||||
|
payload := &SessionRequest{
|
||||||
|
SessionResult: "",
|
||||||
|
}
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
service := s.adyenClient.Checkout()
|
||||||
|
req := service.PaymentsApi.GetResultOfPaymentSessionInput(paymentId).SessionResult(payload.SessionResult)
|
||||||
|
|
||||||
|
res, _, err := service.PaymentsApi.GetResultOfPaymentSession(r.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if res.Status != nil && *res.Status == "completed" {
|
||||||
|
_, err := s.ApplyLocal(r.Context(), checkoutId, &messages.PaymentCompleted{
|
||||||
|
PaymentId: paymentId,
|
||||||
|
Status: *res.Status,
|
||||||
|
ProcessorReference: res.Id,
|
||||||
|
CompletedAt: timestamppb.Now(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("unable to apply payment started mutation", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.WriteResult(w, res)
|
||||||
|
case "klarna":
|
||||||
|
current, err := s.klarnaClient.GetOrder(r.Context(), paymentId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
|
||||||
|
}
|
||||||
|
return s.WriteResult(w, current)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("unsupported payment provider")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckoutPoolServer) StartPaymentHandler(w http.ResponseWriter, r *http.Request, checkoutId checkout.CheckoutId) error {
|
||||||
|
grain, err := s.Get(r.Context(), uint64(checkoutId))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
payload := &StartPayment{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch payload.Provider {
|
||||||
|
case "adyen":
|
||||||
|
meta := GetCheckoutMetaFromRequest(r)
|
||||||
|
sessionData, err := BuildAdyenCheckoutSession(grain, meta)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("unable to build adyen session", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
service := s.adyenClient.Checkout()
|
||||||
|
req := service.PaymentsApi.SessionsInput().CreateCheckoutSessionRequest(*sessionData)
|
||||||
|
session, _, err := service.PaymentsApi.Sessions(r.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("unable to create adyen session", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sessionBytes, err := json.Marshal(session)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("unable to marshal session", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Apply PaymentStarted mutation
|
||||||
|
result, err := s.ApplyLocal(r.Context(), checkoutId, &messages.PaymentStarted{
|
||||||
|
PaymentId: session.Id,
|
||||||
|
Amount: session.Amount.Value,
|
||||||
|
Currency: session.Amount.Currency,
|
||||||
|
Provider: "adyen",
|
||||||
|
SessionData: &anypb.Any{
|
||||||
|
TypeUrl: "type.googleapis.com/google.protobuf.StringValue",
|
||||||
|
Value: sessionBytes,
|
||||||
|
},
|
||||||
|
Method: &payload.Method,
|
||||||
|
StartedAt: timestamppb.New(time.Now()),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("unable to apply payment started mutation", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.WriteResult(w, result)
|
||||||
|
|
||||||
|
case "klarna":
|
||||||
|
order, err := s.CreateOrUpdateCheckout(r, grain, nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("unable to create klarna session", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
orderBytes, err := json.Marshal(order)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("unable to marshal order", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result, err := s.ApplyLocal(r.Context(), checkoutId, &messages.PaymentStarted{
|
||||||
|
PaymentId: order.ID,
|
||||||
|
Amount: int64(order.OrderAmount),
|
||||||
|
Currency: order.PurchaseCurrency,
|
||||||
|
Provider: "klarna",
|
||||||
|
Method: &payload.Method,
|
||||||
|
SessionData: &anypb.Any{
|
||||||
|
TypeUrl: "type.googleapis.com/google.protobuf.StringValue",
|
||||||
|
Value: orderBytes,
|
||||||
|
},
|
||||||
|
StartedAt: timestamppb.New(time.Now()),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("unable to apply payment started mutation", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.WriteResult(w, result)
|
||||||
|
|
||||||
|
default:
|
||||||
|
http.Error(w, "unsupported payment provider", http.StatusBadRequest)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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", CookieCheckoutIdHandler(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/push", s.KlarnaPushHandler)
|
||||||
|
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("POST /api/checkout/start/{cartid}", s.StartCheckoutHandler)
|
||||||
|
handleFunc("GET /api/checkout", CookieCheckoutIdHandler(s.ProxyHandler(s.GetCheckoutHandler)))
|
||||||
|
handleFunc("POST /api/checkout/delivery", CookieCheckoutIdHandler(s.ProxyHandler(s.SetDeliveryHandler)))
|
||||||
|
handleFunc("DELETE /api/checkout/delivery/{id}", CookieCheckoutIdHandler(s.ProxyHandler(s.RemoveDeliveryHandler)))
|
||||||
|
handleFunc("POST /api/checkout/pickup-point", CookieCheckoutIdHandler(s.ProxyHandler(s.SetPickupPointHandler)))
|
||||||
|
handleFunc("POST /api/checkout/contact-details", CookieCheckoutIdHandler(s.ProxyHandler(s.ContactDetailsUpdatedHandler)))
|
||||||
|
handleFunc("POST /payment", CookieCheckoutIdHandler(s.ProxyHandler(s.StartPaymentHandler)))
|
||||||
|
handleFunc("POST /payment/{id}/session", CookieCheckoutIdHandler(s.ProxyHandler(s.GetPaymentSessionHandler)))
|
||||||
|
// handleFunc("POST /api/checkout/initialize", CookieCheckoutIdHandler(s.ProxyHandler(s.InitializeCheckoutHandler)))
|
||||||
|
// handleFunc("POST /api/checkout/inventory-reserved", CookieCheckoutIdHandler(s.ProxyHandler(s.InventoryReservedHandler)))
|
||||||
|
// handleFunc("POST /api/checkout/order-created", CookieCheckoutIdHandler(s.ProxyHandler(s.OrderCreatedHandler)))
|
||||||
|
// handleFunc("POST /api/checkout/confirmation-viewed", CookieCheckoutIdHandler(s.ProxyHandler(s.ConfirmationViewedHandler)))
|
||||||
|
|
||||||
|
//handleFunc("GET /payment/klarna/session", CookieCheckoutIdHandler(s.ProxyHandler(s.KlarnaSessionHandler)))
|
||||||
|
//handleFunc("GET /payment/klarna/checkout", CookieCheckoutIdHandler(s.ProxyHandler(s.KlarnaHtmlCheckoutHandler)))
|
||||||
|
|
||||||
|
handleFunc("GET /payment/klarna/confirmation/{order_id}", s.KlarnaConfirmationHandler)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkoutCookieName = "checkoutid"
|
||||||
|
|
||||||
|
func setCheckoutCookie(w http.ResponseWriter, checkoutId checkout.CheckoutId, tls bool) {
|
||||||
|
if checkoutId == 0 {
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: checkoutCookieName,
|
||||||
|
Value: checkoutId.String(),
|
||||||
|
Secure: tls,
|
||||||
|
HttpOnly: true,
|
||||||
|
Path: "/",
|
||||||
|
Expires: time.Unix(0, 0),
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: checkoutCookieName,
|
||||||
|
Value: checkoutId.String(),
|
||||||
|
Secure: tls,
|
||||||
|
HttpOnly: true,
|
||||||
|
Path: "/",
|
||||||
|
Expires: time.Now().AddDate(0, 0, 14),
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CookieCheckoutIdHandler(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
|
||||||
|
cookie, err := r.Cookie(checkoutCookieName)
|
||||||
|
if err != nil || cookie.Value == "" {
|
||||||
|
w.WriteHeader(http.StatusNotAcceptable)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
parsed, ok := cart.ParseCartId(cookie.Value)
|
||||||
|
if !ok {
|
||||||
|
w.WriteHeader(http.StatusNotAcceptable)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
id = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-15
@@ -6,10 +6,9 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.tornberg.me/mats/go-redis-inventory/pkg/inventory"
|
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
||||||
"github.com/matst80/slask-finder/pkg/index"
|
"github.com/matst80/slask-finder/pkg/index"
|
||||||
"github.com/matst80/slask-finder/pkg/messaging"
|
"github.com/matst80/slask-finder/pkg/messaging"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
@@ -19,7 +18,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
service *inventory.RedisInventoryService
|
inventoryService *inventory.RedisInventoryService
|
||||||
|
reservationService *inventory.RedisCartReservationService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *Server) livezHandler(w http.ResponseWriter, r *http.Request) {
|
func (srv *Server) livezHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -33,17 +33,11 @@ func (srv *Server) readyzHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (srv *Server) getInventoryHandler(w http.ResponseWriter, r *http.Request) {
|
func (srv *Server) getInventoryHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// Parse path: /inventory/{sku}/{location}
|
|
||||||
path := r.URL.Path
|
|
||||||
parts := strings.Split(strings.Trim(path, "/"), "/")
|
|
||||||
if len(parts) != 3 || parts[0] != "inventory" {
|
|
||||||
http.Error(w, "Invalid path", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sku := inventory.SKU(parts[1])
|
|
||||||
locationID := inventory.LocationID(parts[2])
|
|
||||||
|
|
||||||
quantity, err := srv.service.GetInventory(r.Context(), sku, locationID)
|
sku := inventory.SKU(r.PathValue("sku"))
|
||||||
|
locationID := inventory.LocationID(r.PathValue("locationId"))
|
||||||
|
|
||||||
|
quantity, err := srv.inventoryService.GetInventory(r.Context(), sku, locationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -54,6 +48,20 @@ func (srv *Server) getInventoryHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(response)
|
json.NewEncoder(w).Encode(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (srv *Server) getReservationHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
sku := inventory.SKU(r.PathValue("sku"))
|
||||||
|
locationID := inventory.LocationID(r.PathValue("locationId"))
|
||||||
|
|
||||||
|
summary, err := srv.reservationService.GetReservationSummary(r.Context(), sku, locationID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(summary)
|
||||||
|
}
|
||||||
|
|
||||||
var country = "se"
|
var country = "se"
|
||||||
var redisAddress = "10.10.3.18:6379"
|
var redisAddress = "10.10.3.18:6379"
|
||||||
var redisPassword = "slaskredis"
|
var redisPassword = "slaskredis"
|
||||||
@@ -87,12 +95,19 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
server := &Server{service: s}
|
r, err := inventory.NewRedisCartReservationService(rdb)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Unable to connect to reservation redis: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
server := &Server{inventoryService: s, reservationService: r}
|
||||||
|
|
||||||
// Set up HTTP routes
|
// Set up HTTP routes
|
||||||
http.HandleFunc("/livez", server.livezHandler)
|
http.HandleFunc("/livez", server.livezHandler)
|
||||||
http.HandleFunc("/readyz", server.readyzHandler)
|
http.HandleFunc("/readyz", server.readyzHandler)
|
||||||
http.HandleFunc("/inventory/", server.getInventoryHandler)
|
http.HandleFunc("/inventory/{sku}/{locationId}", server.getInventoryHandler)
|
||||||
|
http.HandleFunc("/reservations/{sku}/{locationId}", server.getReservationHandler)
|
||||||
|
|
||||||
stockhandler := &StockHandler{
|
stockhandler := &StockHandler{
|
||||||
MainStockLocationID: inventory.LocationID(country),
|
MainStockLocationID: inventory.LocationID(country),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.tornberg.me/mats/go-redis-inventory/pkg/inventory"
|
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
||||||
"github.com/matst80/slask-finder/pkg/types"
|
"github.com/matst80/slask-finder/pkg/types"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|||||||
+204
-3
@@ -38,7 +38,7 @@ spec:
|
|||||||
volumes:
|
volumes:
|
||||||
- name: data
|
- name: data
|
||||||
nfs:
|
nfs:
|
||||||
path: /i-data/7a8af061/nfs/cart-actor
|
path: /i-data/7a8af061/nfs/
|
||||||
server: 10.10.1.10
|
server: 10.10.1.10
|
||||||
serviceAccountName: default
|
serviceAccountName: default
|
||||||
containers:
|
containers:
|
||||||
@@ -76,8 +76,26 @@ spec:
|
|||||||
memory: "70Mi"
|
memory: "70Mi"
|
||||||
cpu: "1200m"
|
cpu: "1200m"
|
||||||
env:
|
env:
|
||||||
|
- name: DATA_DIR
|
||||||
|
value: "/data/cart-actor"
|
||||||
|
- name: CHECKOUT_DATA_DIR
|
||||||
|
value: "/data/checkout-actor"
|
||||||
- name: TZ
|
- name: TZ
|
||||||
value: "Europe/Stockholm"
|
value: "Europe/Stockholm"
|
||||||
|
- name: REDIS_ADDRESS
|
||||||
|
value: "10.10.3.18:6379"
|
||||||
|
- name: REDIS_PASSWORD
|
||||||
|
value: "slaskredis"
|
||||||
|
- name: ADYEN_HMAC
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: adyen
|
||||||
|
key: HMAC
|
||||||
|
- name: ADYEN_API_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: adyen
|
||||||
|
key: API_KEY
|
||||||
- name: KLARNA_API_USERNAME
|
- name: KLARNA_API_USERNAME
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
@@ -166,6 +184,10 @@ spec:
|
|||||||
memory: "70Mi"
|
memory: "70Mi"
|
||||||
cpu: "1200m"
|
cpu: "1200m"
|
||||||
env:
|
env:
|
||||||
|
- name: DATA_DIR
|
||||||
|
value: "/data/cart-actor"
|
||||||
|
- name: CHECKOUT_DATA_DIR
|
||||||
|
value: "/data/checkout-actor"
|
||||||
- name: TZ
|
- name: TZ
|
||||||
value: "Europe/Stockholm"
|
value: "Europe/Stockholm"
|
||||||
- name: KLARNA_API_USERNAME
|
- name: KLARNA_API_USERNAME
|
||||||
@@ -181,6 +203,16 @@ spec:
|
|||||||
value: "service.name=cart,service.version=0.1.2"
|
value: "service.name=cart,service.version=0.1.2"
|
||||||
- name: OTEL_EXPORTER_OTLP_ENDPOINT
|
- name: OTEL_EXPORTER_OTLP_ENDPOINT
|
||||||
value: "http://otel-debug-service.monitoring:4317"
|
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_PASSWORD
|
- name: KLARNA_API_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
@@ -258,14 +290,14 @@ spec:
|
|||||||
path: /livez
|
path: /livez
|
||||||
port: web
|
port: web
|
||||||
failureThreshold: 1
|
failureThreshold: 1
|
||||||
periodSeconds: 30
|
periodSeconds: 15
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /readyz
|
path: /readyz
|
||||||
port: web
|
port: web
|
||||||
failureThreshold: 2
|
failureThreshold: 2
|
||||||
initialDelaySeconds: 2
|
initialDelaySeconds: 2
|
||||||
periodSeconds: 30
|
periodSeconds: 15
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- mountPath: "/data"
|
- mountPath: "/data"
|
||||||
name: data
|
name: data
|
||||||
@@ -286,6 +318,16 @@ spec:
|
|||||||
value: "service.name=cart,service.version=0.1.2"
|
value: "service.name=cart,service.version=0.1.2"
|
||||||
- name: OTEL_EXPORTER_OTLP_ENDPOINT
|
- name: OTEL_EXPORTER_OTLP_ENDPOINT
|
||||||
value: "http://otel-debug-service.monitoring:4317"
|
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
|
- name: KLARNA_API_USERNAME
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
@@ -323,6 +365,140 @@ 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-cart-actor-amd64:latest
|
||||||
|
name: checkout-actor-amd64
|
||||||
|
imagePullPolicy: Always
|
||||||
|
command: ["/go-checkout-actor"]
|
||||||
|
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: 15
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /readyz
|
||||||
|
port: web
|
||||||
|
failureThreshold: 2
|
||||||
|
initialDelaySeconds: 1
|
||||||
|
periodSeconds: 15
|
||||||
|
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:8080"
|
||||||
|
- 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
|
||||||
@@ -363,6 +539,20 @@ spec:
|
|||||||
name: cart-actor
|
name: cart-actor
|
||||||
port:
|
port:
|
||||||
number: 8080
|
number: 8080
|
||||||
|
- path: /api/checkout
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: checkout-actor
|
||||||
|
port:
|
||||||
|
number: 8080
|
||||||
|
- path: /payment
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: checkout-actor
|
||||||
|
port:
|
||||||
|
number: 8080
|
||||||
---
|
---
|
||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: Ingress
|
kind: Ingress
|
||||||
@@ -458,3 +648,14 @@ spec:
|
|||||||
value: "redis.home:6379"
|
value: "redis.home:6379"
|
||||||
- name: REDIS_PASSWORD
|
- name: REDIS_PASSWORD
|
||||||
value: "slaskredis"
|
value: "slaskredis"
|
||||||
|
---
|
||||||
|
kind: Service
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: inventory
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: cart-inventory
|
||||||
|
ports:
|
||||||
|
- name: web
|
||||||
|
port: 8080
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
module git.tornberg.me/go-cart-actor
|
module git.k6n.net/go-cart-actor
|
||||||
|
|
||||||
go 1.25.3
|
go 1.25.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.tornberg.me/mats/go-redis-inventory v0.0.0-20251113201741-8bf0efac50ee
|
github.com/adyen/adyen-go-api-library/v21 v21.1.0
|
||||||
github.com/gogo/protobuf v1.3.2
|
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/matst80/slask-finder v0.0.0-20251118173753-f66c21cfbda4
|
github.com/matst80/go-redis-inventory v0.0.0-20251126173508-51b30de2d86e
|
||||||
|
github.com/matst80/slask-finder v0.0.0-20251125182907-9e57f193127a
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/rabbitmq/amqp091-go v1.10.0
|
github.com/rabbitmq/amqp091-go v1.10.0
|
||||||
github.com/redis/go-redis/v9 v9.16.0
|
github.com/redis/go-redis/v9 v9.17.0
|
||||||
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0
|
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
|
||||||
go.opentelemetry.io/otel v1.38.0
|
go.opentelemetry.io/otel v1.38.0
|
||||||
@@ -46,19 +46,20 @@ require (
|
|||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.22.3 // indirect
|
github.com/go-openapi/jsonpointer v0.22.3 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.21.3 // indirect
|
github.com/go-openapi/jsonreference v0.21.3 // indirect
|
||||||
github.com/go-openapi/swag v0.25.3 // indirect
|
github.com/go-openapi/swag v0.25.4 // indirect
|
||||||
github.com/go-openapi/swag/cmdutils v0.25.3 // indirect
|
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
|
||||||
github.com/go-openapi/swag/conv v0.25.3 // indirect
|
github.com/go-openapi/swag/conv v0.25.4 // indirect
|
||||||
github.com/go-openapi/swag/fileutils v0.25.3 // indirect
|
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
|
||||||
github.com/go-openapi/swag/jsonname v0.25.3 // indirect
|
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
||||||
github.com/go-openapi/swag/jsonutils v0.25.3 // indirect
|
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
|
||||||
github.com/go-openapi/swag/loading v0.25.3 // indirect
|
github.com/go-openapi/swag/loading v0.25.4 // indirect
|
||||||
github.com/go-openapi/swag/mangling v0.25.3 // indirect
|
github.com/go-openapi/swag/mangling v0.25.4 // indirect
|
||||||
github.com/go-openapi/swag/netutils v0.25.3 // indirect
|
github.com/go-openapi/swag/netutils v0.25.4 // indirect
|
||||||
github.com/go-openapi/swag/stringutils v0.25.3 // indirect
|
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
|
||||||
github.com/go-openapi/swag/typeutils v0.25.3 // indirect
|
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
|
||||||
github.com/go-openapi/swag/yamlutils v0.25.3 // indirect
|
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
|
||||||
github.com/google/gnostic-models v0.7.0 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/google/gnostic-models v0.7.1 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/gorilla/schema v1.4.1 // indirect
|
github.com/gorilla/schema v1.4.1 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||||
@@ -76,13 +77,13 @@ require (
|
|||||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.3 // indirect
|
github.com/prometheus/common v0.67.4 // indirect
|
||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/speakeasy-api/jsonpath v0.6.2 // indirect
|
github.com/speakeasy-api/jsonpath v0.6.2 // indirect
|
||||||
github.com/speakeasy-api/openapi-overlay v0.10.3 // indirect
|
github.com/speakeasy-api/openapi-overlay v0.10.3 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
|
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
|
||||||
github.com/woodsbury/decimal128 v1.3.0 // indirect
|
github.com/woodsbury/decimal128 v1.4.0 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||||
@@ -99,18 +100,18 @@ require (
|
|||||||
golang.org/x/text v0.31.0 // indirect
|
golang.org/x/text v0.31.0 // indirect
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
golang.org/x/tools v0.39.0 // indirect
|
golang.org/x/tools v0.39.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
k8s.io/klog/v2 v2.130.1 // indirect
|
k8s.io/klog/v2 v2.130.1 // indirect
|
||||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
|
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // indirect
|
||||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect
|
||||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
git.tornberg.me/mats/go-redis-inventory v0.0.0-20251113201741-8bf0efac50ee h1:9K/INdO9y/hAjlERsYkJWAla6BUEAPXtChVLfYtWdGI=
|
|
||||||
git.tornberg.me/mats/go-redis-inventory v0.0.0-20251113201741-8bf0efac50ee/go.mod h1:jrDU55O7sdN2RJr99upmig/FAla/mW1Cdju7834TXug=
|
|
||||||
github.com/RoaringBitmap/roaring/v2 v2.14.4 h1:4aKySrrg9G/5oRtJ3TrZLObVqxgQ9f1znCRBwEwjuVw=
|
github.com/RoaringBitmap/roaring/v2 v2.14.4 h1:4aKySrrg9G/5oRtJ3TrZLObVqxgQ9f1znCRBwEwjuVw=
|
||||||
github.com/RoaringBitmap/roaring/v2 v2.14.4/go.mod h1:oMvV6omPWr+2ifRdeZvVJyaz+aoEUopyv5iH0u/+wbY=
|
github.com/RoaringBitmap/roaring/v2 v2.14.4/go.mod h1:oMvV6omPWr+2ifRdeZvVJyaz+aoEUopyv5iH0u/+wbY=
|
||||||
|
github.com/adyen/adyen-go-api-library/v21 v21.1.0 h1:QIKtn99yoBdt2R4PhuMdmY/DTm6Ex5HYd0cB7Sh3y6Y=
|
||||||
|
github.com/adyen/adyen-go-api-library/v21 v21.1.0/go.mod h1:qsAGYetm761eDAz+f2OQoY4qC+tKNhZOHil1b4FO5zE=
|
||||||
|
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
||||||
|
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
|
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
|
||||||
@@ -43,32 +45,32 @@ github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYA
|
|||||||
github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo=
|
github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo=
|
||||||
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
|
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
|
||||||
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
|
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
|
||||||
github.com/go-openapi/swag v0.25.3 h1:FAa5wJXyDtI7yUztKDfZxDrSx+8WTg31MfCQ9s3PV+s=
|
github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU=
|
||||||
github.com/go-openapi/swag v0.25.3/go.mod h1:tX9vI8Mj8Ny+uCEk39I1QADvIPI7lkndX4qCsEqhkS8=
|
github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ=
|
||||||
github.com/go-openapi/swag/cmdutils v0.25.3 h1:EIwGxN143JCThNHnqfqs85R8lJcJG06qjJRZp3VvjLI=
|
github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4=
|
||||||
github.com/go-openapi/swag/cmdutils v0.25.3/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
|
github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
|
||||||
github.com/go-openapi/swag/conv v0.25.3 h1:PcB18wwfba7MN5BVlBIV+VxvUUeC2kEuCEyJ2/t2X7E=
|
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
|
||||||
github.com/go-openapi/swag/conv v0.25.3/go.mod h1:n4Ibfwhn8NJnPXNRhBO5Cqb9ez7alBR40JS4rbASUPU=
|
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
|
||||||
github.com/go-openapi/swag/fileutils v0.25.3 h1:P52Uhd7GShkeU/a1cBOuqIcHMHBrA54Z2t5fLlE85SQ=
|
github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y=
|
||||||
github.com/go-openapi/swag/fileutils v0.25.3/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk=
|
github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk=
|
||||||
github.com/go-openapi/swag/jsonname v0.25.3 h1:U20VKDS74HiPaLV7UZkztpyVOw3JNVsit+w+gTXRj0A=
|
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
|
||||||
github.com/go-openapi/swag/jsonname v0.25.3/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
|
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
|
||||||
github.com/go-openapi/swag/jsonutils v0.25.3 h1:kV7wer79KXUM4Ea4tBdAVTU842Rg6tWstX3QbM4fGdw=
|
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
|
||||||
github.com/go-openapi/swag/jsonutils v0.25.3/go.mod h1:ILcKqe4HC1VEZmJx51cVuZQ6MF8QvdfXsQfiaCs0z9o=
|
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
|
||||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.3 h1:/i3E9hBujtXfHy91rjtwJ7Fgv5TuDHgnSrYjhFxwxOw=
|
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
|
||||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.3/go.mod h1:8kYfCR2rHyOj25HVvxL5Nm8wkfzggddgjZm6RgjT8Ao=
|
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
|
||||||
github.com/go-openapi/swag/loading v0.25.3 h1:Nn65Zlzf4854MY6Ft0JdNrtnHh2bdcS/tXckpSnOb2Y=
|
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
|
||||||
github.com/go-openapi/swag/loading v0.25.3/go.mod h1:xajJ5P4Ang+cwM5gKFrHBgkEDWfLcsAKepIuzTmOb/c=
|
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
|
||||||
github.com/go-openapi/swag/mangling v0.25.3 h1:rGIrEzXaYWuUW1MkFmG3pcH+EIA0/CoUkQnIyB6TUyo=
|
github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48=
|
||||||
github.com/go-openapi/swag/mangling v0.25.3/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg=
|
github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg=
|
||||||
github.com/go-openapi/swag/netutils v0.25.3 h1:XWXHZfL/65ABiv8rvGp9dtE0C6QHTYkCrNV77jTl358=
|
github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0=
|
||||||
github.com/go-openapi/swag/netutils v0.25.3/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg=
|
github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg=
|
||||||
github.com/go-openapi/swag/stringutils v0.25.3 h1:nAmWq1fUTWl/XiaEPwALjp/8BPZJun70iDHRNq/sH6w=
|
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
|
||||||
github.com/go-openapi/swag/stringutils v0.25.3/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
|
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
|
||||||
github.com/go-openapi/swag/typeutils v0.25.3 h1:2w4mEEo7DQt3V4veWMZw0yTPQibiL3ri2fdDV4t2TQc=
|
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
|
||||||
github.com/go-openapi/swag/typeutils v0.25.3/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
|
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
|
||||||
github.com/go-openapi/swag/yamlutils v0.25.3 h1:LKTJjCn/W1ZfMec0XDL4Vxh8kyAnv1orH5F2OREDUrg=
|
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
|
||||||
github.com/go-openapi/swag/yamlutils v0.25.3/go.mod h1:Y7QN6Wc5DOBXK14/xeo1cQlq0EA0wvLoSv13gDQoCao=
|
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
|
||||||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
|
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
|
||||||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
|
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
|
||||||
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
|
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
|
||||||
@@ -82,8 +84,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
|
|||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
|
||||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
@@ -96,6 +98,8 @@ github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4F
|
|||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
@@ -115,8 +119,10 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
|||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
|
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
|
||||||
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||||
github.com/matst80/slask-finder v0.0.0-20251118173753-f66c21cfbda4 h1:yWwWkCaXhwrp5EAd6Z2Rd/n823K0Fz/A3+bqcAcn0qk=
|
github.com/matst80/go-redis-inventory v0.0.0-20251126173508-51b30de2d86e h1:Z7A73W6jsxFuFKWvB1efQmTjs0s7+x2B7IBM2ukkI6Y=
|
||||||
github.com/matst80/slask-finder v0.0.0-20251118173753-f66c21cfbda4/go.mod h1:aqCC0Y1Jv+DhL36YHXf+0bZZkpQNMe9yFMcwgRSJ+Rc=
|
github.com/matst80/go-redis-inventory v0.0.0-20251126173508-51b30de2d86e/go.mod h1:9P52UwIlLWLZvObfO29aKTWUCA9Gm62IuPJ/qv4Xvs0=
|
||||||
|
github.com/matst80/slask-finder v0.0.0-20251125182907-9e57f193127a h1:EfUO5BNDK3a563zQlwJYTNNv46aJFT9gbSItAwZOZ/Y=
|
||||||
|
github.com/matst80/slask-finder v0.0.0-20251125182907-9e57f193127a/go.mod h1:VIPNkIvU0dZKwbSuv75zZcB93MXISm2UyiIPly/ucXQ=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -155,14 +161,14 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
|
|||||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.67.3 h1:shd26MlnwTw5jksTDhC7rTQIteBxy+ZZDr3t7F2xN2Q=
|
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
||||||
github.com/prometheus/common v0.67.3/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
||||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||||
github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4=
|
github.com/redis/go-redis/v9 v9.17.0 h1:K6E+ZlYN95KSMmZeEQPbU/c++wfmEvfFB17yEAq/VhM=
|
||||||
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
github.com/redis/go-redis/v9 v9.17.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||||
@@ -185,12 +191,14 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d
|
|||||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk=
|
github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk=
|
||||||
github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ=
|
github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ=
|
||||||
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
|
github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc=
|
||||||
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
|
github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU=
|
||||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||||
|
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0 h1:bwnLpizECbPr1RrQ27waeY2SPIPeccCx/xLuoYADZ9s=
|
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0 h1:bwnLpizECbPr1RrQ27waeY2SPIPeccCx/xLuoYADZ9s=
|
||||||
@@ -279,10 +287,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
|||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846 h1:ZdyUkS9po3H7G0tuh955QVyyotWvOD4W0aEapeGeUYk=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846/go.mod h1:Fk4kyraUvqD7i5H6S43sj2W98fbZa75lpZz/eUyhfO0=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
@@ -315,15 +323,15 @@ k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M=
|
|||||||
k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE=
|
k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE=
|
||||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ=
|
||||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E=
|
||||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||||
|
|||||||
+6
-6
@@ -18,10 +18,10 @@ This directory contains a k6 script (`cart_load_test.js`) to stress and observe
|
|||||||
|
|
||||||
The script exercises (per iteration):
|
The script exercises (per iteration):
|
||||||
|
|
||||||
1. `GET /cart/` – ensure / fetch cart state (creates cart if missing; sets `cartid` & `cartowner` cookies)
|
1. `GET /cart` – ensure / fetch cart state (creates cart if missing; sets `cartid` & `cartowner` cookies)
|
||||||
2. `POST /cart/` – add item mutation (random SKU & quantity)
|
2. `POST /cart` – add item mutation (random SKU & quantity)
|
||||||
3. `GET /cart/` – fetch after mutations
|
3. `GET /cart` – fetch after mutations
|
||||||
4. `GET /cart/checkout` – occasionally (~2% of iterations) to simulate checkout start
|
4. `GET /checkout` – occasionally (~2% of iterations) to simulate checkout start
|
||||||
|
|
||||||
You can extend it easily to hit deliveries, quantity changes, or removal endpoints.
|
You can extend it easily to hit deliveries, quantity changes, or removal endpoints.
|
||||||
|
|
||||||
@@ -40,9 +40,9 @@ Example run:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
k6 run \
|
k6 run \
|
||||||
-e BASE_URL=https://cart.prod.example.com/cart \
|
-e BASE_URL=https://cart.k6n.net/cart \
|
||||||
-e VUS=40 \
|
-e VUS=40 \
|
||||||
-e DURATION=10m \
|
-e DURATION=1m \
|
||||||
-e RAMP_TARGET=120 \
|
-e RAMP_TARGET=120 \
|
||||||
k6/cart_load_test.js
|
k6/cart_load_test.js
|
||||||
```
|
```
|
||||||
|
|||||||
+166
-170
@@ -4,43 +4,39 @@ import { Counter, Trend } from "k6/metrics";
|
|||||||
|
|
||||||
// ---------------- Configuration ----------------
|
// ---------------- Configuration ----------------
|
||||||
export const options = {
|
export const options = {
|
||||||
// Adjust vus/duration for your environment
|
// Adjust vus/duration for your environment
|
||||||
scenarios: {
|
scenarios: {
|
||||||
steady_mutations: {
|
steady_mutations: {
|
||||||
executor: "constant-vus",
|
executor: "constant-vus",
|
||||||
vus: __ENV.VUS ? parseInt(__ENV.VUS, 10) : 20,
|
vus: __ENV.VUS ? parseInt(__ENV.VUS, 10) : 20,
|
||||||
duration: __ENV.DURATION || "5m",
|
duration: __ENV.DURATION || "5m",
|
||||||
gracefulStop: "30s",
|
gracefulStop: "30s",
|
||||||
},
|
|
||||||
ramp_up: {
|
|
||||||
executor: "ramping-vus",
|
|
||||||
startVUs: 0,
|
|
||||||
stages: [
|
|
||||||
{
|
|
||||||
duration: "1m",
|
|
||||||
target: __ENV.RAMP_TARGET
|
|
||||||
? parseInt(__ENV.RAMP_TARGET, 10)
|
|
||||||
: 50,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
duration: "1m",
|
|
||||||
target: __ENV.RAMP_TARGET
|
|
||||||
? parseInt(__ENV.RAMP_TARGET, 10)
|
|
||||||
: 50,
|
|
||||||
},
|
|
||||||
{ duration: "1m", target: 0 },
|
|
||||||
],
|
|
||||||
gracefulStop: "30s",
|
|
||||||
startTime: "30s",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
thresholds: {
|
ramp_up: {
|
||||||
http_req_failed: ["rate<0.02"], // < 2% failures
|
executor: "ramping-vus",
|
||||||
http_req_duration: ["p(90)<800", "p(99)<1500"], // latency SLO
|
startVUs: 0,
|
||||||
"cart_add_item_duration{op:add}": ["p(90)<800"],
|
stages: [
|
||||||
"cart_fetch_duration{op:get}": ["p(90)<600"],
|
{
|
||||||
|
duration: "1m",
|
||||||
|
target: __ENV.RAMP_TARGET ? parseInt(__ENV.RAMP_TARGET, 10) : 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
duration: "1m",
|
||||||
|
target: __ENV.RAMP_TARGET ? parseInt(__ENV.RAMP_TARGET, 10) : 50,
|
||||||
|
},
|
||||||
|
{ duration: "1m", target: 0 },
|
||||||
|
],
|
||||||
|
gracefulStop: "30s",
|
||||||
|
startTime: "30s",
|
||||||
},
|
},
|
||||||
summaryTrendStats: ["avg", "min", "med", "max", "p(90)", "p(95)", "p(99)"],
|
},
|
||||||
|
thresholds: {
|
||||||
|
http_req_failed: ["rate<0.02"], // < 2% failures
|
||||||
|
http_req_duration: ["p(90)<800", "p(99)<1500"], // latency SLO
|
||||||
|
"cart_add_item_duration{op:add}": ["p(90)<800"],
|
||||||
|
"cart_fetch_duration{op:get}": ["p(90)<600"],
|
||||||
|
},
|
||||||
|
summaryTrendStats: ["avg", "min", "med", "max", "p(90)", "p(95)", "p(99)"],
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------- Metrics ----------------
|
// ---------------- Metrics ----------------
|
||||||
@@ -52,197 +48,197 @@ const checkoutCounter = new Counter("cart_checkout_calls");
|
|||||||
|
|
||||||
// ---------------- SKUs ----------------
|
// ---------------- SKUs ----------------
|
||||||
const SKUS = [
|
const SKUS = [
|
||||||
"778290",
|
"778290",
|
||||||
"778345",
|
"778345",
|
||||||
"778317",
|
"778317",
|
||||||
"778277",
|
"778277",
|
||||||
"778267",
|
"778267",
|
||||||
"778376",
|
"778376",
|
||||||
"778244",
|
"778244",
|
||||||
"778384",
|
"778384",
|
||||||
"778365",
|
"778365",
|
||||||
"778377",
|
"778377",
|
||||||
"778255",
|
"778255",
|
||||||
"778286",
|
"778286",
|
||||||
"778246",
|
"778246",
|
||||||
"778270",
|
"778270",
|
||||||
"778266",
|
"778266",
|
||||||
"778285",
|
"778285",
|
||||||
"778329",
|
"778329",
|
||||||
"778425",
|
"778425",
|
||||||
"778407",
|
"778407",
|
||||||
"778418",
|
"778418",
|
||||||
"778430",
|
"778430",
|
||||||
"778469",
|
"778469",
|
||||||
"778358",
|
"778358",
|
||||||
"778351",
|
"778351",
|
||||||
"778319",
|
"778319",
|
||||||
"778307",
|
"778307",
|
||||||
"778278",
|
"778278",
|
||||||
"778251",
|
"778251",
|
||||||
"778253",
|
"778253",
|
||||||
"778261",
|
"778261",
|
||||||
"778263",
|
"778263",
|
||||||
"778273",
|
"778273",
|
||||||
"778281",
|
"778281",
|
||||||
"778294",
|
"778294",
|
||||||
"778297",
|
"778297",
|
||||||
"778302",
|
"778302",
|
||||||
];
|
];
|
||||||
|
|
||||||
// ---------------- Helpers ----------------
|
// ---------------- Helpers ----------------
|
||||||
function randomSku() {
|
function randomSku() {
|
||||||
return SKUS[Math.floor(Math.random() * SKUS.length)];
|
return SKUS[Math.floor(Math.random() * SKUS.length)];
|
||||||
}
|
}
|
||||||
function randomQty() {
|
function randomQty() {
|
||||||
return 1 + Math.floor(Math.random() * 3); // 1..3
|
return 1 + Math.floor(Math.random() * 3); // 1..3
|
||||||
}
|
}
|
||||||
function baseUrl() {
|
function baseUrl() {
|
||||||
const u = __ENV.BASE_URL || "http://localhost:8080/cart";
|
const u = __ENV.BASE_URL || "http://localhost:8080/cart";
|
||||||
// Allow user to pass either root host or full /cart path
|
// Allow user to pass either root host or full /cart path
|
||||||
return u.endsWith("/cart") ? u : u.replace(/\/+$/, "") + "/cart";
|
return u.endsWith("/cart") ? u : u.replace(/\/+$/, "") + "/cart";
|
||||||
}
|
}
|
||||||
function extractCookie(res, name) {
|
function extractCookie(res, name) {
|
||||||
const cookies = res.cookies[name];
|
const cookies = res.cookies[name];
|
||||||
if (!cookies || cookies.length === 0) return null;
|
if (!cookies || cookies.length === 0) return null;
|
||||||
return cookies[0].value;
|
return cookies[0].value;
|
||||||
}
|
}
|
||||||
function withCookies(headers, cookieJar) {
|
function withCookies(headers, cookieJar) {
|
||||||
if (!cookieJar || Object.keys(cookieJar).length === 0) return headers;
|
if (!cookieJar || Object.keys(cookieJar).length === 0) return headers;
|
||||||
const cookieStr = Object.entries(cookieJar)
|
const cookieStr = Object.entries(cookieJar)
|
||||||
.map(([k, v]) => `${k}=${v}`)
|
.map(([k, v]) => `${k}=${v}`)
|
||||||
.join("; ");
|
.join("; ");
|
||||||
return { ...headers, Cookie: cookieStr };
|
return { ...headers, Cookie: cookieStr };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maintain cart + owner cookies per VU
|
// Maintain cart + owner cookies per VU
|
||||||
let cartState = {
|
let cartState = {
|
||||||
cartid: null,
|
cartid: null,
|
||||||
cartowner: null,
|
cartowner: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Refresh cookies from response
|
// Refresh cookies from response
|
||||||
function updateCookies(res) {
|
function updateCookies(res) {
|
||||||
const cid = extractCookie(res, "cartid");
|
const cid = extractCookie(res, "cartid");
|
||||||
if (cid) cartState.cartid = cid;
|
if (cid) cartState.cartid = cid;
|
||||||
const owner = extractCookie(res, "cartowner");
|
const owner = extractCookie(res, "cartowner");
|
||||||
if (owner) cartState.cartowner = owner;
|
if (owner) cartState.cartowner = owner;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build headers
|
// Build headers
|
||||||
function headers() {
|
function headers() {
|
||||||
const h = { "Content-Type": "application/json" };
|
const h = { "Content-Type": "application/json" };
|
||||||
const jar = {};
|
const jar = {};
|
||||||
if (cartState.cartid) jar["cartid"] = cartState.cartid;
|
if (cartState.cartid) jar["cartid"] = cartState.cartid;
|
||||||
if (cartState.cartowner) jar["cartowner"] = cartState.cartowner;
|
if (cartState.cartowner) jar["cartowner"] = cartState.cartowner;
|
||||||
return withCookies(h, jar);
|
return withCookies(h, jar);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure cart exists (GET /)
|
// Ensure cart exists (GET /)
|
||||||
function ensureCart() {
|
function ensureCart() {
|
||||||
if (cartState.cartid) return;
|
if (cartState.cartid) return;
|
||||||
const res = http.get(baseUrl() + "/", { headers: headers() });
|
const res = http.get(baseUrl(), { headers: headers() });
|
||||||
updateCookies(res);
|
updateCookies(res);
|
||||||
check(res, {
|
check(res, {
|
||||||
"ensure cart status 200": (r) => r.status === 200,
|
"ensure cart status 200": (r) => r.status === 200,
|
||||||
"ensure cart has id": () => !!cartState.cartid,
|
"ensure cart has id": () => !!cartState.cartid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add random item
|
// Add random item
|
||||||
function addRandomItem() {
|
function addRandomItem() {
|
||||||
const payload = JSON.stringify({
|
const payload = JSON.stringify({
|
||||||
sku: randomSku(),
|
sku: randomSku(),
|
||||||
quantity: randomQty(),
|
quantity: randomQty(),
|
||||||
country: "no",
|
country: "no",
|
||||||
});
|
});
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
const res = http.post(baseUrl(), payload, { headers: headers() });
|
const res = http.post(baseUrl(), payload, { headers: headers() });
|
||||||
const dur = Date.now() - start;
|
const dur = Date.now() - start;
|
||||||
addItemTrend.add(dur, { op: "add" });
|
addItemTrend.add(dur, { op: "add" });
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
addedItemsCounter.add(1);
|
addedItemsCounter.add(1);
|
||||||
}
|
}
|
||||||
updateCookies(res);
|
updateCookies(res);
|
||||||
check(res, {
|
check(res, {
|
||||||
"add item status ok": (r) => r.status === 200,
|
"add item status ok": (r) => r.status === 200,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch cart state
|
// Fetch cart state
|
||||||
function fetchCart() {
|
function fetchCart() {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
const res = http.get(baseUrl() + "/", { headers: headers() });
|
const res = http.get(baseUrl(), { headers: headers() });
|
||||||
const dur = Date.now() - start;
|
const dur = Date.now() - start;
|
||||||
fetchTrend.add(dur, { op: "get" });
|
fetchTrend.add(dur, { op: "get" });
|
||||||
updateCookies(res);
|
updateCookies(res);
|
||||||
check(res, { "fetch status ok": (r) => r.status === 200 });
|
check(res, { "fetch status ok": (r) => r.status === 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Occasional checkout trigger
|
// Occasional checkout trigger
|
||||||
function maybeCheckout() {
|
function maybeCheckout() {
|
||||||
if (!cartState.cartid) return;
|
if (!cartState.cartid) return;
|
||||||
// // Small probability
|
// // Small probability
|
||||||
// if (Math.random() < 0.02) {
|
// if (Math.random() < 0.02) {
|
||||||
// const start = Date.now();
|
// const start = Date.now();
|
||||||
// const res = http.get(baseUrl() + "/checkout", { headers: headers() });
|
// const res = http.get(baseUrl() + "/checkout", { headers: headers() });
|
||||||
// const dur = Date.now() - start;
|
// const dur = Date.now() - start;
|
||||||
// checkoutTrend.add(dur, { op: "checkout" });
|
// checkoutTrend.add(dur, { op: "checkout" });
|
||||||
// updateCookies(res);
|
// updateCookies(res);
|
||||||
// if (res.status === 200) checkoutCounter.add(1);
|
// if (res.status === 200) checkoutCounter.add(1);
|
||||||
// check(res, { "checkout status ok": (r) => r.status === 200 });
|
// check(res, { "checkout status ok": (r) => r.status === 200 });
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- k6 lifecycle ----------------
|
// ---------------- k6 lifecycle ----------------
|
||||||
export function setup() {
|
export function setup() {
|
||||||
// Provide SKU list length for summary
|
// Provide SKU list length for summary
|
||||||
return { skuCount: SKUS.length };
|
return { skuCount: SKUS.length };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function (data) {
|
export default function (data) {
|
||||||
group("cart flow", () => {
|
group("cart flow", () => {
|
||||||
// Create or reuse cart
|
// Create or reuse cart
|
||||||
ensureCart();
|
ensureCart();
|
||||||
|
|
||||||
// Random number of item mutations per iteration (1..5)
|
// Random number of item mutations per iteration (1..5)
|
||||||
const ops = 1 + Math.floor(Math.random() * 5);
|
const ops = 1 + Math.floor(Math.random() * 5);
|
||||||
for (let i = 0; i < ops; i++) {
|
for (let i = 0; i < ops; i++) {
|
||||||
addRandomItem();
|
addRandomItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch state
|
// Fetch state
|
||||||
fetchCart();
|
fetchCart();
|
||||||
|
|
||||||
// Optional checkout attempt
|
// Optional checkout attempt
|
||||||
maybeCheckout();
|
maybeCheckout();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Small think time
|
// Small think time
|
||||||
sleep(Math.random() * 0.5);
|
sleep(Math.random() * 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function teardown(data) {
|
export function teardown(data) {
|
||||||
// Optionally we could GET confirmation or clear cart cookie
|
// Optionally we could GET confirmation or clear cart cookie
|
||||||
// Not implemented for load purpose.
|
// Not implemented for load purpose.
|
||||||
console.log(`Test complete. SKU count: ${data.skuCount}`);
|
console.log(`Test complete. SKU count: ${data.skuCount}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- Summary ----------------
|
// ---------------- Summary ----------------
|
||||||
export function handleSummary(data) {
|
export function handleSummary(data) {
|
||||||
return {
|
return {
|
||||||
stdout: JSON.stringify(
|
stdout: JSON.stringify(
|
||||||
{
|
{
|
||||||
metrics: {
|
metrics: {
|
||||||
mutations_avg: data.metrics.cart_add_item_duration?.avg,
|
mutations_avg: data.metrics.cart_add_item_duration?.avg,
|
||||||
mutations_p95: data.metrics.cart_add_item_duration?.p(95),
|
mutations_p95: data.metrics.cart_add_item_duration?.p(95),
|
||||||
fetch_p95: data.metrics.cart_fetch_duration?.p(95),
|
fetch_p95: data.metrics.cart_fetch_duration?.p(95),
|
||||||
checkout_count: data.metrics.cart_checkout_calls?.count,
|
checkout_count: data.metrics.cart_checkout_calls?.count,
|
||||||
},
|
},
|
||||||
checks: data.root_checks,
|
checks: data.root_checks,
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package actor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GrainId uint64
|
||||||
|
|
||||||
|
const base62Alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||||
|
|
||||||
|
// Reverse lookup (0xFF marks invalid)
|
||||||
|
var base62Rev [256]byte
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for i := range base62Rev {
|
||||||
|
base62Rev[i] = 0xFF
|
||||||
|
}
|
||||||
|
for i := 0; i < len(base62Alphabet); i++ {
|
||||||
|
base62Rev[base62Alphabet[i]] = byte(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the canonical base62 encoding of the 64-bit id.
|
||||||
|
func (id GrainId) String() string {
|
||||||
|
return encodeBase62(uint64(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON encodes the cart id as a JSON string.
|
||||||
|
func (id GrainId) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(id.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON decodes a cart id from a JSON string containing base62 text.
|
||||||
|
func (id *GrainId) UnmarshalJSON(data []byte) error {
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal(data, &s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
parsed, ok := ParseGrainId(s)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid cart id: %q", s)
|
||||||
|
}
|
||||||
|
*id = parsed
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGrainId generates a new cryptographically random non-zero 64-bit id.
|
||||||
|
func NewGrainId() (GrainId, error) {
|
||||||
|
var b [8]byte
|
||||||
|
if _, err := rand.Read(b[:]); err != nil {
|
||||||
|
return 0, fmt.Errorf("NewGrainId: %w", err)
|
||||||
|
}
|
||||||
|
u := (uint64(b[0]) << 56) |
|
||||||
|
(uint64(b[1]) << 48) |
|
||||||
|
(uint64(b[2]) << 40) |
|
||||||
|
(uint64(b[3]) << 32) |
|
||||||
|
(uint64(b[4]) << 24) |
|
||||||
|
(uint64(b[5]) << 16) |
|
||||||
|
(uint64(b[6]) << 8) |
|
||||||
|
uint64(b[7])
|
||||||
|
if u == 0 {
|
||||||
|
// Extremely unlikely; regenerate once to avoid "0" identifier if desired.
|
||||||
|
return NewGrainId()
|
||||||
|
}
|
||||||
|
return GrainId(u), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustNewGrainId panics if generation fails.
|
||||||
|
func MustNewGrainId() GrainId {
|
||||||
|
id, err := NewGrainId()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseGrainId parses a base62 string into a GrainId.
|
||||||
|
// Returns (0,false) for invalid input.
|
||||||
|
func ParseGrainId(s string) (GrainId, bool) {
|
||||||
|
// Accept length 1..11 (11 sufficient for 64 bits). Reject >11 immediately.
|
||||||
|
// Provide a slightly looser upper bound (<=16) only if you anticipate future
|
||||||
|
// extensions; here we stay strict.
|
||||||
|
if len(s) == 0 || len(s) > 11 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
u, ok := decodeBase62(s)
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return GrainId(u), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustParseGrainId panics on invalid base62 input.
|
||||||
|
func MustParseGrainId(s string) GrainId {
|
||||||
|
id, ok := ParseGrainId(s)
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("invalid cart id: %q", s))
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeBase62 converts a uint64 to base62 (shortest form).
|
||||||
|
func encodeBase62(u uint64) string {
|
||||||
|
if u == 0 {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
var buf [11]byte
|
||||||
|
i := len(buf)
|
||||||
|
for u > 0 {
|
||||||
|
i--
|
||||||
|
buf[i] = base62Alphabet[u%62]
|
||||||
|
u /= 62
|
||||||
|
}
|
||||||
|
return string(buf[i:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeBase62 converts base62 text to uint64.
|
||||||
|
func decodeBase62(s string) (uint64, bool) {
|
||||||
|
var v uint64
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
c := s[i]
|
||||||
|
d := base62Rev[c]
|
||||||
|
if d == 0xFF {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
v = v*62 + uint64(d)
|
||||||
|
}
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gogo/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
type QueueEvent struct {
|
type QueueEvent struct {
|
||||||
|
|||||||
+11
-9
@@ -2,9 +2,10 @@ package actor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gogo/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MutationResult[V any] struct {
|
type MutationResult[V any] struct {
|
||||||
@@ -14,28 +15,29 @@ type MutationResult[V any] struct {
|
|||||||
|
|
||||||
type GrainPool[V any] interface {
|
type GrainPool[V any] interface {
|
||||||
Apply(ctx context.Context, id uint64, mutation ...proto.Message) (*MutationResult[V], error)
|
Apply(ctx context.Context, id uint64, mutation ...proto.Message) (*MutationResult[V], error)
|
||||||
Get(ctx context.Context, id uint64) (V, error)
|
Get(ctx context.Context, id uint64) (*V, error)
|
||||||
OwnerHost(id uint64) (Host, bool)
|
OwnerHost(id uint64) (Host[V], bool)
|
||||||
Hostname() string
|
Hostname() string
|
||||||
TakeOwnership(id uint64)
|
TakeOwnership(id uint64)
|
||||||
HandleOwnershipChange(host string, ids []uint64) error
|
HandleOwnershipChange(host string, ids []uint64) error
|
||||||
HandleRemoteExpiry(host string, ids []uint64) error
|
HandleRemoteExpiry(host string, ids []uint64) error
|
||||||
Negotiate(otherHosts []string)
|
Negotiate(otherHosts []string)
|
||||||
GetLocalIds() []uint64
|
GetLocalIds() []uint64
|
||||||
|
IsHealthy() bool
|
||||||
|
Close()
|
||||||
|
IsKnown(string) bool
|
||||||
RemoveHost(host string)
|
RemoveHost(host string)
|
||||||
AddRemoteHost(host string)
|
AddRemoteHost(host string)
|
||||||
IsHealthy() bool
|
|
||||||
IsKnown(string) bool
|
|
||||||
GetPubSub() *PubSub
|
|
||||||
Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host abstracts a remote node capable of proxying cart requests.
|
// Host abstracts a remote node capable of proxying cart requests.
|
||||||
type Host interface {
|
type Host[V any] interface {
|
||||||
AnnounceExpiry(ids []uint64)
|
AnnounceExpiry(ids []uint64)
|
||||||
Negotiate(otherHosts []string) ([]string, error)
|
Negotiate(otherHosts []string) ([]string, error)
|
||||||
Name() string
|
Name() string
|
||||||
Proxy(id uint64, w http.ResponseWriter, r *http.Request) (bool, error)
|
Proxy(id uint64, w http.ResponseWriter, r *http.Request, customBody io.Reader) (bool, error)
|
||||||
|
Apply(ctx context.Context, id uint64, mutation ...proto.Message) (*MutationResult[V], error)
|
||||||
|
Get(ctx context.Context, id uint64) (*V, error)
|
||||||
GetActorIds() []uint64
|
GetActorIds() []uint64
|
||||||
Close() error
|
Close() error
|
||||||
Ping() bool
|
Ping() bool
|
||||||
|
|||||||
@@ -2,18 +2,21 @@ package actor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
messages "git.tornberg.me/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"
|
||||||
"go.opentelemetry.io/otel/metric"
|
"go.opentelemetry.io/otel/metric"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/reflection"
|
"google.golang.org/grpc/reflection"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"google.golang.org/protobuf/types/known/anypb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ControlServer implements the ControlPlane gRPC services.
|
// ControlServer implements the ControlPlane gRPC services.
|
||||||
@@ -97,13 +100,37 @@ func (s *ControlServer[V]) AnnounceOwnership(ctx context.Context, req *messages.
|
|||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Ack count: %d", len(req.Ids))
|
// log.Printf("Ack count: %d", len(req.Ids))
|
||||||
return &messages.OwnerChangeAck{
|
return &messages.OwnerChangeAck{
|
||||||
Accepted: true,
|
Accepted: true,
|
||||||
Message: "ownership announced",
|
Message: "ownership announced",
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toAny[V any](grain V) (*anypb.Any, error) {
|
||||||
|
data, err := json.Marshal(grain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &anypb.Any{
|
||||||
|
Value: data,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ControlServer[V]) Get(ctx context.Context, req *messages.GetRequest) (*messages.GetReply, error) {
|
||||||
|
grain, err := s.pool.Get(ctx, req.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
grainAny, err := toAny(grain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &messages.GetReply{
|
||||||
|
Grain: grainAny,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ControlServer[V]) AnnounceExpiry(ctx context.Context, req *messages.ExpiryAnnounce) (*messages.OwnerChangeAck, error) {
|
func (s *ControlServer[V]) AnnounceExpiry(ctx context.Context, req *messages.ExpiryAnnounce) (*messages.OwnerChangeAck, error) {
|
||||||
ctx, span := tracer.Start(ctx, "grpc_announce_expiry")
|
ctx, span := tracer.Start(ctx, "grpc_announce_expiry")
|
||||||
defer span.End()
|
defer span.End()
|
||||||
@@ -126,7 +153,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()
|
||||||
|
|
||||||
@@ -139,6 +166,47 @@ func (s *ControlServer[V]) Ping(ctx context.Context, _ *messages.Empty) (*messag
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ControlServer[V]) Apply(ctx context.Context, in *messages.ApplyRequest) (*messages.ApplyResult, error) {
|
||||||
|
msgs := make([]proto.Message, len(in.Messages))
|
||||||
|
for i, anyMsg := range in.Messages {
|
||||||
|
msg, err := anyMsg.UnmarshalNew()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal message: %w", err)
|
||||||
|
}
|
||||||
|
msgs[i] = msg
|
||||||
|
}
|
||||||
|
r, err := s.pool.Apply(ctx, in.Id, msgs...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
grainAny, err := toAny(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mutList := make([]*messages.MutationResult, len(in.Messages))
|
||||||
|
for i, msg := range r.Mutations {
|
||||||
|
mut, err := anypb.New(msg.Mutation)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var errString *string
|
||||||
|
if msg.Error != nil {
|
||||||
|
s := msg.Error.Error()
|
||||||
|
errString = &s
|
||||||
|
}
|
||||||
|
mutList[i] = &messages.MutationResult{
|
||||||
|
Type: msg.Type,
|
||||||
|
Message: mut,
|
||||||
|
Error: errString,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &messages.ApplyResult{
|
||||||
|
State: grainAny,
|
||||||
|
Mutations: mutList,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ControlPlane: Negotiate (merge host views)
|
// ControlPlane: Negotiate (merge host views)
|
||||||
func (s *ControlServer[V]) Negotiate(ctx context.Context, req *messages.NegotiateRequest) (*messages.NegotiateReply, error) {
|
func (s *ControlServer[V]) Negotiate(ctx context.Context, req *messages.NegotiateRequest) (*messages.NegotiateReply, error) {
|
||||||
ctx, span := tracer.Start(ctx, "grpc_negotiate")
|
ctx, span := tracer.Start(ctx, "grpc_negotiate")
|
||||||
@@ -155,7 +223,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()
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package actor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
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/credentials/insecure"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"google.golang.org/protobuf/types/known/anypb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockGrainPool for testing
|
||||||
|
type mockGrainPool struct {
|
||||||
|
applied []proto.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockGrainPool) Apply(ctx context.Context, id uint64, mutations ...proto.Message) (*MutationResult[mockGrain], error) {
|
||||||
|
m.applied = mutations
|
||||||
|
// Simulate successful application
|
||||||
|
return &MutationResult[mockGrain]{
|
||||||
|
Result: mockGrain{},
|
||||||
|
Mutations: []ApplyResult{
|
||||||
|
{Type: "AddItem", Mutation: &cart_messages.AddItem{ItemId: 1, Quantity: 2}, Error: nil},
|
||||||
|
{Type: "RemoveItem", Mutation: &cart_messages.RemoveItem{Id: 1}, Error: nil},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockGrainPool) Get(ctx context.Context, id uint64) (*mockGrain, error) {
|
||||||
|
return &mockGrain{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockGrainPool) OwnerHost(id uint64) (Host[mockGrain], bool) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockGrainPool) TakeOwnership(id uint64) {}
|
||||||
|
|
||||||
|
func (m *mockGrainPool) Hostname() string { return "test-host" }
|
||||||
|
|
||||||
|
func (m *mockGrainPool) HandleOwnershipChange(host string, ids []uint64) error { return nil }
|
||||||
|
func (m *mockGrainPool) HandleRemoteExpiry(host string, ids []uint64) error { return nil }
|
||||||
|
func (m *mockGrainPool) Negotiate(hosts []string) {}
|
||||||
|
func (m *mockGrainPool) GetLocalIds() []uint64 { return []uint64{} }
|
||||||
|
func (m *mockGrainPool) RemoveHost(host string) {}
|
||||||
|
func (m *mockGrainPool) AddRemoteHost(host string) {}
|
||||||
|
func (m *mockGrainPool) IsHealthy() bool { return true }
|
||||||
|
func (m *mockGrainPool) IsKnown(host string) bool { return false }
|
||||||
|
func (m *mockGrainPool) Close() {}
|
||||||
|
|
||||||
|
type mockGrain struct{}
|
||||||
|
|
||||||
|
func TestApplyRequestWithMutations(t *testing.T) {
|
||||||
|
// Setup mock pool
|
||||||
|
pool := &mockGrainPool{}
|
||||||
|
|
||||||
|
// Create gRPC server
|
||||||
|
server, err := NewControlServer[mockGrain](DefaultServerConfig(), pool)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create server: %v", err)
|
||||||
|
}
|
||||||
|
defer server.GracefulStop()
|
||||||
|
|
||||||
|
// Create client connection
|
||||||
|
conn, err := grpc.Dial("localhost:1337", grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to dial: %v", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client := control_plane_messages.NewControlPlaneClient(conn)
|
||||||
|
|
||||||
|
// Prepare ApplyRequest with multiple Any messages
|
||||||
|
addItemAny, _ := anypb.New(&cart_messages.AddItem{ItemId: 1, Quantity: 2})
|
||||||
|
removeItemAny, _ := anypb.New(&cart_messages.RemoveItem{Id: 1})
|
||||||
|
req := &control_plane_messages.ApplyRequest{
|
||||||
|
Id: 123,
|
||||||
|
Messages: []*anypb.Any{addItemAny, removeItemAny},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call Apply
|
||||||
|
resp, err := client.Apply(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Apply failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify response
|
||||||
|
if resp.State == nil {
|
||||||
|
t.Errorf("expected State to be non-nil")
|
||||||
|
}
|
||||||
|
if len(resp.Mutations) != 2 {
|
||||||
|
t.Errorf("expected 2 mutation results, got %d", len(resp.Mutations))
|
||||||
|
}
|
||||||
|
for i, mut := range resp.Mutations {
|
||||||
|
if mut.Error != nil {
|
||||||
|
t.Errorf("expected no error in mutation %d, got %s", i, *mut.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify mutations were extracted and applied
|
||||||
|
if len(pool.applied) != 2 {
|
||||||
|
t.Errorf("expected 2 mutations applied, got %d", len(pool.applied))
|
||||||
|
}
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRequest(t *testing.T) {
|
||||||
|
// Setup mock pool
|
||||||
|
pool := &mockGrainPool{}
|
||||||
|
|
||||||
|
// Create gRPC server
|
||||||
|
server, err := NewControlServer[mockGrain](DefaultServerConfig(), pool)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create server: %v", err)
|
||||||
|
}
|
||||||
|
defer server.GracefulStop()
|
||||||
|
|
||||||
|
// Create client connection
|
||||||
|
conn, err := grpc.Dial("localhost:1337", grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to dial: %v", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client := control_plane_messages.NewControlPlaneClient(conn)
|
||||||
|
|
||||||
|
// Prepare GetRequest
|
||||||
|
req := &control_plane_messages.GetRequest{
|
||||||
|
Id: 123,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call Get
|
||||||
|
resp, err := client.Get(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify response
|
||||||
|
if resp.Grain == nil {
|
||||||
|
t.Errorf("expected Grain to be non-nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/gogo/protobuf/proto"
|
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ApplyResult struct {
|
type ApplyResult struct {
|
||||||
@@ -41,14 +41,16 @@ type MutationRegistry interface {
|
|||||||
Create(typeName string) (proto.Message, bool)
|
Create(typeName string) (proto.Message, bool)
|
||||||
GetTypeName(msg proto.Message) (string, bool)
|
GetTypeName(msg proto.Message) (string, bool)
|
||||||
RegisterProcessor(processor ...MutationProcessor)
|
RegisterProcessor(processor ...MutationProcessor)
|
||||||
//GetStorageEvent(msg proto.Message) StorageEvent
|
RegisterTrigger(trigger ...TriggerHandler)
|
||||||
//FromStorageEvent(event StorageEvent) (proto.Message, error)
|
SetEventChannel(ch chan<- ApplyResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProtoMutationRegistry struct {
|
type ProtoMutationRegistry struct {
|
||||||
mutationRegistryMu sync.RWMutex
|
mutationRegistryMu sync.RWMutex
|
||||||
mutationRegistry map[reflect.Type]MutationHandler
|
mutationRegistry map[reflect.Type]MutationHandler
|
||||||
|
triggers map[reflect.Type][]TriggerHandler
|
||||||
processors []MutationProcessor
|
processors []MutationProcessor
|
||||||
|
eventChannel chan<- ApplyResult
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -84,6 +86,26 @@ func WithTotals() MutationOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TriggerHandler interface {
|
||||||
|
Handle(state any, msg proto.Message) []proto.Message
|
||||||
|
Name() string
|
||||||
|
Type() reflect.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegisteredTrigger[V any, I proto.Message] struct {
|
||||||
|
name string
|
||||||
|
handler func(state any, msg proto.Message) []proto.Message
|
||||||
|
msgType reflect.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTrigger[V any, I proto.Message](name string, handler func(state any, msg proto.Message) []proto.Message) *RegisteredTrigger[V, I] {
|
||||||
|
return &RegisteredTrigger[V, I]{
|
||||||
|
name: name,
|
||||||
|
handler: handler,
|
||||||
|
msgType: reflect.TypeOf((*I)(nil)).Elem(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type MutationHandler interface {
|
type MutationHandler interface {
|
||||||
Handle(state any, msg proto.Message) error
|
Handle(state any, msg proto.Message) error
|
||||||
Name() string
|
Name() string
|
||||||
@@ -95,17 +117,26 @@ 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 {
|
||||||
|
var t T
|
||||||
|
rt := reflect.TypeOf(t)
|
||||||
|
if rt != nil && rt.Kind() == reflect.Pointer {
|
||||||
|
return reflect.New(rt.Elem()).Interface().(proto.Message)
|
||||||
|
}
|
||||||
|
log.Fatalf("expected to create proto message got %+v", rt)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
instance := create()
|
instance := create()
|
||||||
rt := reflect.TypeOf(instance)
|
rt := reflect.TypeOf(instance)
|
||||||
if rt.Kind() == reflect.Ptr {
|
if rt.Kind() == reflect.Pointer {
|
||||||
rt = rt.Elem()
|
rt = rt.Elem()
|
||||||
}
|
}
|
||||||
return &RegisteredMutation[V, T]{
|
return &RegisteredMutation[V, T]{
|
||||||
@@ -136,6 +167,7 @@ func NewMutationRegistry() MutationRegistry {
|
|||||||
return &ProtoMutationRegistry{
|
return &ProtoMutationRegistry{
|
||||||
mutationRegistry: make(map[reflect.Type]MutationHandler),
|
mutationRegistry: make(map[reflect.Type]MutationHandler),
|
||||||
mutationRegistryMu: sync.RWMutex{},
|
mutationRegistryMu: sync.RWMutex{},
|
||||||
|
triggers: make(map[reflect.Type][]TriggerHandler),
|
||||||
processors: make([]MutationProcessor, 0),
|
processors: make([]MutationProcessor, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,6 +185,24 @@ func (r *ProtoMutationRegistry) RegisterMutations(handlers ...MutationHandler) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ProtoMutationRegistry) RegisterTrigger(triggers ...TriggerHandler) {
|
||||||
|
r.mutationRegistryMu.Lock()
|
||||||
|
defer r.mutationRegistryMu.Unlock()
|
||||||
|
|
||||||
|
for _, trigger := range triggers {
|
||||||
|
existingTriggers, ok := r.triggers[trigger.Type()]
|
||||||
|
if !ok {
|
||||||
|
r.triggers[trigger.Type()] = []TriggerHandler{trigger}
|
||||||
|
} else {
|
||||||
|
r.triggers[trigger.Type()] = append(existingTriggers, trigger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProtoMutationRegistry) SetEventChannel(ch chan<- ApplyResult) {
|
||||||
|
r.eventChannel = ch
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ProtoMutationRegistry) GetTypeName(msg proto.Message) (string, bool) {
|
func (r *ProtoMutationRegistry) GetTypeName(msg proto.Message) (string, bool) {
|
||||||
r.mutationRegistryMu.RLock()
|
r.mutationRegistryMu.RLock()
|
||||||
defer r.mutationRegistryMu.RUnlock()
|
defer r.mutationRegistryMu.RUnlock()
|
||||||
@@ -212,13 +262,13 @@ func (r *ProtoMutationRegistry) Apply(ctx context.Context, grain any, msg ...pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, m := range msg {
|
for _, m := range msg {
|
||||||
// Ignore nil mutation elements (untyped or typed nil pointers) silently; they carry no data.
|
// Error if any mutation element is nil.
|
||||||
if m == nil {
|
if m == nil {
|
||||||
continue
|
return results, fmt.Errorf("nil mutation message")
|
||||||
}
|
}
|
||||||
// Typed nil: interface holds concrete proto message type whose pointer value is nil.
|
// Typed nil: interface holds concrete proto message type whose pointer value is nil.
|
||||||
rv := reflect.ValueOf(m)
|
rv := reflect.ValueOf(m)
|
||||||
if rv.Kind() == reflect.Ptr && rv.IsNil() {
|
if rv.Kind() == reflect.Pointer && rv.IsNil() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
rt := indirectType(reflect.TypeOf(m))
|
rt := indirectType(reflect.TypeOf(m))
|
||||||
@@ -235,6 +285,25 @@ func (r *ProtoMutationRegistry) Apply(ctx context.Context, grain any, msg ...pro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
msgSpan.RecordError(err)
|
msgSpan.RecordError(err)
|
||||||
}
|
}
|
||||||
|
if r.eventChannel != nil {
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
// Handle panic from sending to closed channel
|
||||||
|
log.Printf("event channel closed: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for _, tr := range r.triggers[rt] {
|
||||||
|
for _, msg := range tr.Handle(grain, m) {
|
||||||
|
select {
|
||||||
|
case r.eventChannel <- msg:
|
||||||
|
default:
|
||||||
|
// Channel full or no receiver, skip to avoid blocking
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
results = append(results, ApplyResult{Error: err, Type: rt.Name(), Mutation: m})
|
results = append(results, ApplyResult{Error: err, Type: rt.Name(), Mutation: m})
|
||||||
}
|
}
|
||||||
msgSpan.End()
|
msgSpan.End()
|
||||||
@@ -251,6 +320,13 @@ func (r *ProtoMutationRegistry) Apply(ctx context.Context, grain any, msg ...pro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Return error for unregistered mutations
|
||||||
|
for _, res := range results {
|
||||||
|
if res.Error == ErrMutationNotRegistered {
|
||||||
|
return results, res.Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,38 +2,37 @@ package actor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.tornberg.me/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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,18 +46,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +66,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +77,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)
|
||||||
}
|
}
|
||||||
@@ -90,7 +89,7 @@ func TestRegisteredMutationBasics(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply nil grain
|
// Apply nil grain
|
||||||
if _, err := reg.Apply(nil, add); err == nil {
|
if _, err := reg.Apply(context.Background(), nil, add); err == nil {
|
||||||
t.Fatalf("expected error for nil grain")
|
t.Fatalf("expected error for nil grain")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,36 +99,109 @@ func TestRegisteredMutationBasics(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply unregistered message
|
// Apply unregistered message
|
||||||
if _, err := reg.Apply(context.Background(), state, &messages.Noop{}); !errors.Is(err, ErrMutationNotRegistered) {
|
_, err := reg.Apply(context.Background(), state, &cart_messages.RemoveItem{})
|
||||||
|
if err != ErrMutationNotRegistered {
|
||||||
t.Fatalf("expected ErrMutationNotRegistered, got %v", err)
|
t.Fatalf("expected ErrMutationNotRegistered, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// func TestConcurrentSafeRegistrationLookup(t *testing.T) {
|
func TestEventChannel(t *testing.T) {
|
||||||
// // This test is light-weight; it ensures locks don't deadlock under simple concurrent access.
|
reg := NewMutationRegistry().(*ProtoMutationRegistry)
|
||||||
// reg := NewMutationRegistry().(*ProtoMutationRegistry)
|
|
||||||
// mut := NewMutation[cartState, *messages.Noop](
|
|
||||||
// func(state *cartState, msg *messages.Noop) error { state.calls++; return nil },
|
|
||||||
// func() *messages.Noop { return &messages.Noop{} },
|
|
||||||
// )
|
|
||||||
// reg.RegisterMutations(mut)
|
|
||||||
|
|
||||||
// done := make(chan struct{})
|
addItemMutation := NewMutation(
|
||||||
// const workers = 25
|
func(state *cartState, msg *cart_messages.AddItem) error {
|
||||||
// for i := 0; i < workers; i++ {
|
state.calls++
|
||||||
// go func() {
|
return nil
|
||||||
// for j := 0; j < 100; j++ {
|
},
|
||||||
// _, _ = reg.Create("Noop")
|
)
|
||||||
// _, _ = reg.GetTypeName(&messages.Noop{})
|
|
||||||
// _ = reg.Apply(&cartState{}, &messages.Noop{})
|
|
||||||
// }
|
|
||||||
// done <- struct{}{}
|
|
||||||
// }()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// for i := 0; i < workers; i++ {
|
reg.RegisterMutations(addItemMutation)
|
||||||
// <-done
|
|
||||||
// }
|
eventCh := make(chan ApplyResult, 10)
|
||||||
// }
|
reg.SetEventChannel(eventCh)
|
||||||
|
|
||||||
|
state := &cartState{}
|
||||||
|
add := &cart_messages.AddItem{ItemId: 42, Quantity: 3, Sku: "ABC"}
|
||||||
|
results, err := reg.Apply(context.Background(), state, add)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Apply returned error: %v", err)
|
||||||
|
}
|
||||||
|
if len(results) != 1 {
|
||||||
|
t.Fatalf("expected 1 result, got %d", len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receive from channel with timeout
|
||||||
|
select {
|
||||||
|
case res := <-eventCh:
|
||||||
|
if res.Type != "AddItem" {
|
||||||
|
t.Fatalf("expected type AddItem, got %s", res.Type)
|
||||||
|
}
|
||||||
|
if res.Error != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", res.Error)
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatalf("expected to receive event on channel within timeout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventChannelClosed(t *testing.T) {
|
||||||
|
reg := NewMutationRegistry().(*ProtoMutationRegistry)
|
||||||
|
|
||||||
|
addItemMutation := NewMutation(
|
||||||
|
func(state *cartState, msg *cart_messages.AddItem) error {
|
||||||
|
state.calls++
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
reg.RegisterMutations(addItemMutation)
|
||||||
|
|
||||||
|
eventCh := make(chan ApplyResult, 10)
|
||||||
|
reg.SetEventChannel(eventCh)
|
||||||
|
|
||||||
|
close(eventCh) // Close the channel to simulate external close
|
||||||
|
|
||||||
|
state := &cartState{}
|
||||||
|
add := &cart_messages.AddItem{ItemId: 42, Quantity: 3, Sku: "ABC"}
|
||||||
|
// This should not panic due to recover in goroutine
|
||||||
|
results, err := reg.Apply(context.Background(), state, add)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Apply returned error: %v", err)
|
||||||
|
}
|
||||||
|
if len(results) != 1 {
|
||||||
|
t.Fatalf("expected 1 result, got %d", len(results))
|
||||||
|
}
|
||||||
|
// Test passes if no panic occurs
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEventChannelUnbufferedNoListener(t *testing.T) {
|
||||||
|
reg := NewMutationRegistry().(*ProtoMutationRegistry)
|
||||||
|
|
||||||
|
addItemMutation := NewMutation(
|
||||||
|
func(state *cartState, msg *cart_messages.AddItem) error {
|
||||||
|
state.calls++
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
reg.RegisterMutations(addItemMutation)
|
||||||
|
|
||||||
|
eventCh := make(chan ApplyResult) // unbuffered
|
||||||
|
reg.SetEventChannel(eventCh)
|
||||||
|
|
||||||
|
// No goroutine reading from eventCh
|
||||||
|
|
||||||
|
state := &cartState{}
|
||||||
|
add := &cart_messages.AddItem{ItemId: 42, Quantity: 3, Sku: "ABC"}
|
||||||
|
results, err := reg.Apply(context.Background(), state, add)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Apply returned error: %v", err)
|
||||||
|
}
|
||||||
|
if len(results) != 1 {
|
||||||
|
t.Fatalf("expected 1 result, got %d", len(results))
|
||||||
|
}
|
||||||
|
// Since no listener, the send should go to default and not block
|
||||||
|
// Test passes if Apply completes without hanging
|
||||||
|
}
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
|
|||||||
+42
-57
@@ -1,93 +1,78 @@
|
|||||||
package actor
|
package actor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"iter"
|
||||||
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Event represents an event to be published.
|
type ReceiverFunc[V any] func(event V)
|
||||||
type Event struct {
|
|
||||||
Topic string
|
|
||||||
Payload interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotifyFunc is a function to notify a grain of an event.
|
type PubSub[V any] struct {
|
||||||
type NotifyFunc func(grainID uint64, event Event)
|
subscribers []*ReceiverFunc[V]
|
||||||
|
|
||||||
// Subscribable is an interface for grains that can receive notifications.
|
|
||||||
type Subscribable interface {
|
|
||||||
Notify(event Event)
|
|
||||||
UpdateSubscriptions(pubsub *PubSub)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PubSub manages subscriptions for grains to topics.
|
|
||||||
// Topics are strings, e.g., "sku:12345"
|
|
||||||
// Subscribers are grain IDs (uint64)
|
|
||||||
type PubSub struct {
|
|
||||||
subscribers map[string][]uint64
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
notify NotifyFunc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPubSub creates a new PubSub instance.
|
// NewPubSub creates a new PubSub instance.
|
||||||
func NewPubSub(notify NotifyFunc) *PubSub {
|
func NewPubSub[V any]() *PubSub[V] {
|
||||||
return &PubSub{
|
return &PubSub[V]{
|
||||||
subscribers: make(map[string][]uint64),
|
subscribers: make([]*ReceiverFunc[V], 0),
|
||||||
notify: notify,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe adds a grain ID to the subscribers of a topic.
|
// Subscribe adds a grain ID to the subscribers of a topic.
|
||||||
func (p *PubSub) Subscribe(topic string, grainID uint64) {
|
func (p *PubSub[V]) Subscribe(receiver ReceiverFunc[V]) {
|
||||||
|
if receiver == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
p.subscribers[topic] = append(p.subscribers[topic], grainID)
|
// log.Printf("adding subscriber")
|
||||||
|
p.subscribers = append(p.subscribers, &receiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unsubscribe removes a grain ID from the subscribers of a topic.
|
// Unsubscribe removes a grain ID from the subscribers of a topic.
|
||||||
func (p *PubSub) Unsubscribe(topic string, grainID uint64) {
|
func (p *PubSub[V]) Unsubscribe(receiver ReceiverFunc[V]) {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
list := p.subscribers[topic]
|
list := p.subscribers
|
||||||
for i, id := range list {
|
prt := &receiver
|
||||||
if id == grainID {
|
for i, sub := range list {
|
||||||
p.subscribers[topic] = append(list[:i], list[i+1:]...)
|
if sub == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if sub == prt {
|
||||||
|
// log.Printf("removing subscriber")
|
||||||
|
p.subscribers = append(list[:i], list[i+1:]...)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
p.subscribers = slices.DeleteFunc(p.subscribers, func(fn *ReceiverFunc[V]) bool {
|
||||||
|
return fn == nil
|
||||||
|
})
|
||||||
// If list is empty, could delete, but not necessary
|
// If list is empty, could delete, but not necessary
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnsubscribeAll removes the grain ID from all topics.
|
|
||||||
func (p *PubSub) UnsubscribeAll(grainID uint64) {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
for topic, list := range p.subscribers {
|
|
||||||
newList := make([]uint64, 0, len(list))
|
|
||||||
for _, id := range list {
|
|
||||||
if id != grainID {
|
|
||||||
newList = append(newList, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(newList) == 0 {
|
|
||||||
delete(p.subscribers, topic)
|
|
||||||
} else {
|
|
||||||
p.subscribers[topic] = newList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSubscribers returns a copy of the subscriber IDs for a topic.
|
// GetSubscribers returns a copy of the subscriber IDs for a topic.
|
||||||
func (p *PubSub) GetSubscribers(topic string) []uint64 {
|
func (p *PubSub[V]) GetSubscribers() iter.Seq[ReceiverFunc[V]] {
|
||||||
p.mu.RLock()
|
p.mu.RLock()
|
||||||
defer p.mu.RUnlock()
|
defer p.mu.RUnlock()
|
||||||
list := p.subscribers[topic]
|
|
||||||
return append([]uint64(nil), list...)
|
return func(yield func(ReceiverFunc[V]) bool) {
|
||||||
|
for _, sub := range p.subscribers {
|
||||||
|
if sub == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !yield(*sub) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish sends an event to all subscribers of the topic.
|
// Publish sends an event to all subscribers of the topic.
|
||||||
func (p *PubSub) Publish(event Event) {
|
func (p *PubSub[V]) Publish(event V) {
|
||||||
subs := p.GetSubscribers(event.Topic)
|
for notify := range p.GetSubscribers() {
|
||||||
for _, id := range subs {
|
notify(event)
|
||||||
p.notify(id, event)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gogo/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SimpleGrainPool[V any] struct {
|
type SimpleGrainPool[V any] struct {
|
||||||
@@ -17,18 +17,18 @@ type SimpleGrainPool[V any] struct {
|
|||||||
grains map[uint64]Grain[V]
|
grains map[uint64]Grain[V]
|
||||||
mutationRegistry MutationRegistry
|
mutationRegistry MutationRegistry
|
||||||
spawn func(ctx context.Context, id uint64) (Grain[V], error)
|
spawn func(ctx context.Context, id uint64) (Grain[V], error)
|
||||||
spawnHost func(host string) (Host, error)
|
destroy func(grain Grain[V]) error
|
||||||
|
spawnHost func(host string) (Host[V], error)
|
||||||
listeners []LogListener
|
listeners []LogListener
|
||||||
storage LogStorage[V]
|
storage LogStorage[V]
|
||||||
ttl time.Duration
|
ttl time.Duration
|
||||||
poolSize int
|
poolSize int
|
||||||
pubsub *PubSub
|
|
||||||
|
|
||||||
// Cluster coordination --------------------------------------------------
|
// Cluster coordination --------------------------------------------------
|
||||||
hostname string
|
hostname string
|
||||||
remoteMu sync.RWMutex
|
remoteMu sync.RWMutex
|
||||||
remoteOwners map[uint64]Host
|
remoteOwners map[uint64]Host[V]
|
||||||
remoteHosts map[string]Host
|
remoteHosts map[string]Host[V]
|
||||||
//discardedHostHandler *DiscardedHostHandler
|
//discardedHostHandler *DiscardedHostHandler
|
||||||
|
|
||||||
// House-keeping ---------------------------------------------------------
|
// House-keeping ---------------------------------------------------------
|
||||||
@@ -38,7 +38,8 @@ type SimpleGrainPool[V any] struct {
|
|||||||
type GrainPoolConfig[V any] struct {
|
type GrainPoolConfig[V any] struct {
|
||||||
Hostname string
|
Hostname string
|
||||||
Spawn func(ctx context.Context, id uint64) (Grain[V], error)
|
Spawn func(ctx context.Context, id uint64) (Grain[V], error)
|
||||||
SpawnHost func(host string) (Host, error)
|
SpawnHost func(host string) (Host[V], error)
|
||||||
|
Destroy func(grain Grain[V]) error
|
||||||
TTL time.Duration
|
TTL time.Duration
|
||||||
PoolSize int
|
PoolSize int
|
||||||
MutationRegistry MutationRegistry
|
MutationRegistry MutationRegistry
|
||||||
@@ -52,11 +53,12 @@ func NewSimpleGrainPool[V any](config GrainPoolConfig[V]) (*SimpleGrainPool[V],
|
|||||||
storage: config.Storage,
|
storage: config.Storage,
|
||||||
spawn: config.Spawn,
|
spawn: config.Spawn,
|
||||||
spawnHost: config.SpawnHost,
|
spawnHost: config.SpawnHost,
|
||||||
|
destroy: config.Destroy,
|
||||||
ttl: config.TTL,
|
ttl: config.TTL,
|
||||||
poolSize: config.PoolSize,
|
poolSize: config.PoolSize,
|
||||||
hostname: config.Hostname,
|
hostname: config.Hostname,
|
||||||
remoteOwners: make(map[uint64]Host),
|
remoteOwners: make(map[uint64]Host[V]),
|
||||||
remoteHosts: make(map[string]Host),
|
remoteHosts: make(map[string]Host[V]),
|
||||||
}
|
}
|
||||||
|
|
||||||
p.purgeTicker = time.NewTicker(time.Minute)
|
p.purgeTicker = time.NewTicker(time.Minute)
|
||||||
@@ -89,14 +91,15 @@ func (p *SimpleGrainPool[V]) purge() {
|
|||||||
for id, grain := range p.grains {
|
for id, grain := range p.grains {
|
||||||
if grain.GetLastAccess().Before(purgeLimit) {
|
if grain.GetLastAccess().Before(purgeLimit) {
|
||||||
purgedIds = append(purgedIds, id)
|
purgedIds = append(purgedIds, id)
|
||||||
if p.pubsub != nil {
|
if err := p.destroy(grain); err != nil {
|
||||||
p.pubsub.UnsubscribeAll(id)
|
log.Printf("failed to destroy grain %d: %v", id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(p.grains, id)
|
delete(p.grains, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.localMu.Unlock()
|
p.localMu.Unlock()
|
||||||
p.forAllHosts(func(remote Host) {
|
p.forAllHosts(func(remote Host[V]) {
|
||||||
remote.AnnounceExpiry(purgedIds)
|
remote.AnnounceExpiry(purgedIds)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -133,7 +136,6 @@ func (p *SimpleGrainPool[V]) HandleRemoteExpiry(host string, ids []uint64) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *SimpleGrainPool[V]) HandleOwnershipChange(host string, ids []uint64) error {
|
func (p *SimpleGrainPool[V]) HandleOwnershipChange(host string, ids []uint64) error {
|
||||||
log.Printf("host %s now owns %d cart ids", host, len(ids))
|
|
||||||
p.remoteMu.RLock()
|
p.remoteMu.RLock()
|
||||||
remoteHost, exists := p.remoteHosts[host]
|
remoteHost, exists := p.remoteHosts[host]
|
||||||
p.remoteMu.RUnlock()
|
p.remoteMu.RUnlock()
|
||||||
@@ -165,7 +167,7 @@ func (p *SimpleGrainPool[V]) AddRemoteHost(host string) {
|
|||||||
p.AddRemote(host)
|
p.AddRemote(host)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SimpleGrainPool[V]) AddRemote(host string) (Host, error) {
|
func (p *SimpleGrainPool[V]) AddRemote(host string) (Host[V], error) {
|
||||||
if host == "" {
|
if host == "" {
|
||||||
return nil, fmt.Errorf("host is empty")
|
return nil, fmt.Errorf("host is empty")
|
||||||
}
|
}
|
||||||
@@ -197,7 +199,7 @@ func (p *SimpleGrainPool[V]) AddRemote(host string) (Host, error) {
|
|||||||
return remote, nil
|
return remote, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SimpleGrainPool[V]) initializeRemote(remote Host) {
|
func (p *SimpleGrainPool[V]) initializeRemote(remote Host[V]) {
|
||||||
|
|
||||||
remotesIds := remote.GetActorIds()
|
remotesIds := remote.GetActorIds()
|
||||||
|
|
||||||
@@ -265,7 +267,7 @@ func (p *SimpleGrainPool[V]) IsKnown(host string) bool {
|
|||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SimpleGrainPool[V]) pingLoop(remote Host) {
|
func (p *SimpleGrainPool[V]) pingLoop(remote Host[V]) {
|
||||||
remote.Ping()
|
remote.Ping()
|
||||||
ticker := time.NewTicker(5 * time.Second)
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
@@ -313,14 +315,14 @@ func (p *SimpleGrainPool[V]) SendNegotiation() {
|
|||||||
p.remoteMu.RLock()
|
p.remoteMu.RLock()
|
||||||
hosts := make([]string, 0, len(p.remoteHosts)+1)
|
hosts := make([]string, 0, len(p.remoteHosts)+1)
|
||||||
hosts = append(hosts, p.hostname)
|
hosts = append(hosts, p.hostname)
|
||||||
remotes := make([]Host, 0, len(p.remoteHosts))
|
remotes := make([]Host[V], 0, len(p.remoteHosts))
|
||||||
for h, r := range p.remoteHosts {
|
for h, r := range p.remoteHosts {
|
||||||
hosts = append(hosts, h)
|
hosts = append(hosts, h)
|
||||||
remotes = append(remotes, r)
|
remotes = append(remotes, r)
|
||||||
}
|
}
|
||||||
p.remoteMu.RUnlock()
|
p.remoteMu.RUnlock()
|
||||||
|
|
||||||
p.forAllHosts(func(remote Host) {
|
p.forAllHosts(func(remote Host[V]) {
|
||||||
knownByRemote, err := remote.Negotiate(hosts)
|
knownByRemote, err := remote.Negotiate(hosts)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -335,7 +337,7 @@ func (p *SimpleGrainPool[V]) SendNegotiation() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SimpleGrainPool[V]) forAllHosts(fn func(Host)) {
|
func (p *SimpleGrainPool[V]) forAllHosts(fn func(Host[V])) {
|
||||||
p.remoteMu.RLock()
|
p.remoteMu.RLock()
|
||||||
rh := maps.Clone(p.remoteHosts)
|
rh := maps.Clone(p.remoteHosts)
|
||||||
p.remoteMu.RUnlock()
|
p.remoteMu.RUnlock()
|
||||||
@@ -363,7 +365,7 @@ func (p *SimpleGrainPool[V]) broadcastOwnership(ids []uint64) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p.forAllHosts(func(rh Host) {
|
p.forAllHosts(func(rh Host[V]) {
|
||||||
rh.AnnounceOwnership(p.hostname, ids)
|
rh.AnnounceOwnership(p.hostname, ids)
|
||||||
})
|
})
|
||||||
log.Printf("%s taking ownership of %d ids", p.hostname, len(ids))
|
log.Printf("%s taking ownership of %d ids", p.hostname, len(ids))
|
||||||
@@ -382,10 +384,11 @@ func (p *SimpleGrainPool[V]) getOrClaimGrain(ctx context.Context, id uint64) (Gr
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
go p.broadcastOwnership([]uint64{id})
|
||||||
p.localMu.Lock()
|
p.localMu.Lock()
|
||||||
p.grains[id] = grain
|
p.grains[id] = grain
|
||||||
p.localMu.Unlock()
|
p.localMu.Unlock()
|
||||||
go p.broadcastOwnership([]uint64{id})
|
|
||||||
return grain, nil
|
return grain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +396,7 @@ func (p *SimpleGrainPool[V]) getOrClaimGrain(ctx context.Context, id uint64) (Gr
|
|||||||
// var ErrNotOwner = fmt.Errorf("not owner")
|
// var ErrNotOwner = fmt.Errorf("not owner")
|
||||||
|
|
||||||
// Apply applies a mutation to a grain.
|
// Apply applies a mutation to a grain.
|
||||||
func (p *SimpleGrainPool[V]) Apply(ctx context.Context, id uint64, mutation ...proto.Message) (*MutationResult[*V], error) {
|
func (p *SimpleGrainPool[V]) Apply(ctx context.Context, id uint64, mutation ...proto.Message) (*MutationResult[V], error) {
|
||||||
grain, err := p.getOrClaimGrain(ctx, id)
|
grain, err := p.getOrClaimGrain(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -417,13 +420,9 @@ func (p *SimpleGrainPool[V]) Apply(ctx context.Context, id uint64, mutation ...p
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if p.pubsub != nil {
|
|
||||||
if sub, ok := any(grain).(Subscribable); ok {
|
return &MutationResult[V]{
|
||||||
sub.UpdateSubscriptions(p.pubsub)
|
Result: *result,
|
||||||
}
|
|
||||||
}
|
|
||||||
return &MutationResult[*V]{
|
|
||||||
Result: result,
|
|
||||||
Mutations: mutations,
|
Mutations: mutations,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -438,7 +437,7 @@ func (p *SimpleGrainPool[V]) Get(ctx context.Context, id uint64) (*V, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OwnerHost reports the remote owner (if any) for the supplied cart id.
|
// OwnerHost reports the remote owner (if any) for the supplied cart id.
|
||||||
func (p *SimpleGrainPool[V]) OwnerHost(id uint64) (Host, bool) {
|
func (p *SimpleGrainPool[V]) OwnerHost(id uint64) (Host[V], bool) {
|
||||||
p.remoteMu.RLock()
|
p.remoteMu.RLock()
|
||||||
defer p.remoteMu.RUnlock()
|
defer p.remoteMu.RUnlock()
|
||||||
owner, ok := p.remoteOwners[id]
|
owner, ok := p.remoteOwners[id]
|
||||||
@@ -450,25 +449,10 @@ func (p *SimpleGrainPool[V]) Hostname() string {
|
|||||||
return p.hostname
|
return p.hostname
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPubSub returns the pubsub instance.
|
|
||||||
func (p *SimpleGrainPool[V]) GetPubSub() *PubSub {
|
|
||||||
return p.pubsub
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *SimpleGrainPool[V]) SetPubSub(pubsub *PubSub) {
|
|
||||||
p.pubsub = pubsub
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *SimpleGrainPool[V]) Publish(event Event) {
|
|
||||||
if p.pubsub != nil {
|
|
||||||
p.pubsub.Publish(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close notifies remotes that this host is shutting down.
|
// Close notifies remotes that this host is shutting down.
|
||||||
func (p *SimpleGrainPool[V]) Close() {
|
func (p *SimpleGrainPool[V]) Close() {
|
||||||
|
|
||||||
p.forAllHosts(func(rh Host) {
|
p.forAllHosts(func(rh Host[V]) {
|
||||||
rh.Close()
|
rh.Close()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gogo/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StateStorage struct {
|
type StateStorage struct {
|
||||||
|
|||||||
+61
-139
@@ -2,14 +2,11 @@ package cart
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.tornberg.me/go-cart-actor/pkg/actor"
|
"git.k6n.net/go-cart-actor/pkg/voucher"
|
||||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
||||||
"git.tornberg.me/go-cart-actor/pkg/voucher"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Legacy padded [16]byte CartId and its helper methods removed.
|
// Legacy padded [16]byte CartId and its helper methods removed.
|
||||||
@@ -25,11 +22,9 @@ type ItemMeta struct {
|
|||||||
Category3 string `json:"category3,omitempty"`
|
Category3 string `json:"category3,omitempty"`
|
||||||
Category4 string `json:"category4,omitempty"`
|
Category4 string `json:"category4,omitempty"`
|
||||||
Category5 string `json:"category5,omitempty"`
|
Category5 string `json:"category5,omitempty"`
|
||||||
SellerId string `json:"sellerId,omitempty"`
|
|
||||||
SellerName string `json:"sellerName,omitempty"`
|
SellerName string `json:"sellerName,omitempty"`
|
||||||
Image string `json:"image,omitempty"`
|
Image string `json:"image,omitempty"`
|
||||||
Outlet *string `json:"outlet,omitempty"`
|
Outlet *string `json:"outlet,omitempty"`
|
||||||
Cgm string `json:"cgm,omitempty"` // Customer Group Membership
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CartItem struct {
|
type CartItem struct {
|
||||||
@@ -39,28 +34,23 @@ type CartItem struct {
|
|||||||
Sku string `json:"sku"`
|
Sku string `json:"sku"`
|
||||||
Price Price `json:"price"`
|
Price Price `json:"price"`
|
||||||
TotalPrice Price `json:"totalPrice"`
|
TotalPrice Price `json:"totalPrice"`
|
||||||
|
SellerId string `json:"sellerId,omitempty"`
|
||||||
OrgPrice *Price `json:"orgPrice,omitempty"`
|
OrgPrice *Price `json:"orgPrice,omitempty"`
|
||||||
|
Cgm string `json:"cgm,omitempty"`
|
||||||
Tax int
|
Tax int
|
||||||
Stock StockStatus `json:"stock"`
|
Stock uint16 `json:"stock"`
|
||||||
Quantity int `json:"qty"`
|
Quantity uint16 `json:"qty"`
|
||||||
Discount *Price `json:"discount,omitempty"`
|
Discount *Price `json:"discount,omitempty"`
|
||||||
Disclaimer string `json:"disclaimer,omitempty"`
|
Disclaimer string `json:"disclaimer,omitempty"`
|
||||||
ArticleType string `json:"type,omitempty"`
|
ArticleType string `json:"type,omitempty"`
|
||||||
StoreId *string `json:"storeId,omitempty"`
|
StoreId *string `json:"storeId,omitempty"`
|
||||||
Meta *ItemMeta `json:"meta,omitempty"`
|
Meta *ItemMeta `json:"meta,omitempty"`
|
||||||
SaleStatus string `json:"saleStatus"`
|
SaleStatus string `json:"saleStatus"`
|
||||||
Marking *Marking `json:"marking,omitempty"`
|
Marking *Marking `json:"marking,omitempty"`
|
||||||
SubscriptionDetailsId string `json:"subscriptionDetailsId,omitempty"`
|
SubscriptionDetailsId string `json:"subscriptionDetailsId,omitempty"`
|
||||||
OrderReference string `json:"orderReference,omitempty"`
|
OrderReference string `json:"orderReference,omitempty"`
|
||||||
IsSubscribed bool `json:"isSubscribed,omitempty"`
|
IsSubscribed bool `json:"isSubscribed,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 {
|
||||||
@@ -84,43 +74,46 @@ type Notice struct {
|
|||||||
Code *string `json:"code,omitempty"`
|
Code *string `json:"code,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CartPaymentStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CartPaymentStatusPending CartPaymentStatus = "pending"
|
||||||
|
CartPaymentStatusFailed CartPaymentStatus = "failed"
|
||||||
|
CartPaymentStatusSuccess CartPaymentStatus = "success"
|
||||||
|
CartPaymentStatusCancelled CartPaymentStatus = "partial"
|
||||||
|
)
|
||||||
|
|
||||||
type Marking struct {
|
type Marking struct {
|
||||||
Type uint32 `json:"type"`
|
Type uint32 `json:"type"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CartGrain struct {
|
type CartGrain struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
lastItemId uint32
|
lastItemId uint32
|
||||||
lastDeliveryId uint32
|
lastVoucherId uint32
|
||||||
lastVoucherId 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"`
|
||||||
InventoryReserved bool `json:"inventoryReserved"`
|
Language string `json:"language"`
|
||||||
Id CartId `json:"id"`
|
Version uint `json:"version"`
|
||||||
Items []*CartItem `json:"items"`
|
InventoryReserved bool `json:"inventoryReserved"`
|
||||||
TotalPrice *Price `json:"totalPrice"`
|
Id CartId `json:"id"`
|
||||||
TotalDiscount *Price `json:"totalDiscount"`
|
Items []*CartItem `json:"items"`
|
||||||
Deliveries []*CartDelivery `json:"deliveries,omitempty"`
|
TotalPrice *Price `json:"totalPrice"`
|
||||||
Processing bool `json:"processing"`
|
TotalDiscount *Price `json:"totalDiscount"`
|
||||||
PaymentInProgress bool `json:"paymentInProgress"`
|
Processing bool `json:"processing"`
|
||||||
OrderReference string `json:"orderReference,omitempty"`
|
//PaymentInProgress uint16 `json:"paymentInProgress"`
|
||||||
PaymentStatus string `json:"paymentStatus,omitempty"`
|
OrderReference string `json:"orderReference,omitempty"`
|
||||||
Vouchers []*Voucher `json:"vouchers,omitempty"`
|
|
||||||
Notifications []CartNotification `json:"cartNotification,omitempty"`
|
|
||||||
SubscriptionDetails map[string]*SubscriptionDetails `json:"subscriptionDetails,omitempty"`
|
|
||||||
PaymentDeclinedNotices []Notice `json:"paymentDeclinedNotices,omitempty"`
|
|
||||||
Confirmation *ConfirmationStatus `json:"confirmation,omitempty"`
|
|
||||||
CheckoutOrderId string `json:"checkoutOrderId,omitempty"`
|
|
||||||
CheckoutStatus string `json:"checkoutStatus,omitempty"`
|
|
||||||
CheckoutCountry string `json:"checkoutCountry,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfirmationStatus struct {
|
Vouchers []*Voucher `json:"vouchers,omitempty"`
|
||||||
Code *string `json:"code,omitempty"`
|
Notifications []CartNotification `json:"cartNotification,omitempty"`
|
||||||
ViewCount int `json:"viewCount"`
|
SubscriptionDetails map[string]*SubscriptionDetails `json:"subscriptionDetails,omitempty"`
|
||||||
LastViewedAt time.Time `json:"lastViewedAt"`
|
|
||||||
|
//CheckoutOrderId string `json:"checkoutOrderId,omitempty"`
|
||||||
|
CheckoutStatus *CartPaymentStatus `json:"checkoutStatus,omitempty"`
|
||||||
|
//CheckoutCountry string `json:"checkoutCountry,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Voucher struct {
|
type Voucher struct {
|
||||||
@@ -183,13 +176,11 @@ 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,
|
||||||
lastAccess: ts,
|
lastAccess: ts,
|
||||||
lastChange: ts,
|
lastChange: ts,
|
||||||
TotalDiscount: NewPrice(),
|
TotalDiscount: NewPrice(),
|
||||||
Vouchers: []*Voucher{},
|
Vouchers: []*Voucher{},
|
||||||
Deliveries: []*CartDelivery{},
|
|
||||||
Id: CartId(id),
|
Id: CartId(id),
|
||||||
Items: []*CartItem{},
|
Items: []*CartItem{},
|
||||||
TotalPrice: NewPrice(),
|
TotalPrice: NewPrice(),
|
||||||
@@ -214,37 +205,16 @@ func (c *CartGrain) GetCurrentState() (*CartGrain, error) {
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify handles incoming events, e.g., inventory changes.
|
func (c *CartGrain) HandleInventoryChange(change inventory.InventoryChange) {
|
||||||
func (c *CartGrain) Notify(event actor.Event) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
// Example: if event is inventory change for a SKU in the cart
|
|
||||||
if strings.HasPrefix(event.Topic, "inventory:") {
|
|
||||||
sku := strings.TrimPrefix(event.Topic, "inventory:")
|
|
||||||
for _, item := range c.Items {
|
|
||||||
if item.Sku == sku {
|
|
||||||
// Update stock status based on payload, e.g., if payload is bool available
|
|
||||||
if available, ok := event.Payload.(bool); ok {
|
|
||||||
if available {
|
|
||||||
item.Stock = StockStatus(1) // assuming 1 is in stock
|
|
||||||
} else {
|
|
||||||
item.Stock = StockStatus(0) // out of stock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CartGrain) UpdateSubscriptions(pubsub *actor.PubSub) {
|
|
||||||
pubsub.UnsubscribeAll(c.GetId())
|
|
||||||
skuSet := make(map[string]bool)
|
|
||||||
for _, item := range c.Items {
|
for _, item := range c.Items {
|
||||||
skuSet[item.Sku] = true
|
l := "se"
|
||||||
}
|
if item.StoreId != nil {
|
||||||
for sku := range skuSet {
|
l = *item.StoreId
|
||||||
pubsub.Subscribe("inventory:"+sku, c.GetId())
|
}
|
||||||
|
if item.Sku == change.SKU && change.StockLocationID == l {
|
||||||
|
item.Stock = uint16(change.Value)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,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()
|
||||||
@@ -290,26 +233,6 @@ func (c *CartGrain) FindItemWithSku(sku string) (*CartItem, bool) {
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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()
|
||||||
@@ -334,9 +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 _, voucher := range c.Vouchers {
|
for _, voucher := range c.Vouchers {
|
||||||
_, ok := voucher.AppliesTo(c)
|
_, ok := voucher.AppliesTo(c)
|
||||||
voucher.Applied = false
|
voucher.Applied = false
|
||||||
@@ -351,4 +272,5 @@ func (c *CartGrain) UpdateTotals() {
|
|||||||
c.TotalPrice.Subtract(*value)
|
c.TotalPrice.Subtract(*value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +1,86 @@
|
|||||||
package cart
|
package cart
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.tornberg.me/go-cart-actor/pkg/actor"
|
"context"
|
||||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
"time"
|
||||||
|
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||||
|
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewCartMultationRegistry() actor.MutationRegistry {
|
type CartMutationContext struct {
|
||||||
|
reservationService inventory.CartReservationService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCartMutationContext(reservationService inventory.CartReservationService) *CartMutationContext {
|
||||||
|
return &CartMutationContext{
|
||||||
|
reservationService: reservationService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CartMutationContext) ReserveItem(ctx context.Context, cartId CartId, sku string, locationId *string, quantity uint16) (*time.Time, error) {
|
||||||
|
|
||||||
|
if quantity <= 0 || c.reservationService == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
l := inventory.LocationID("se")
|
||||||
|
if locationId != nil {
|
||||||
|
l = inventory.LocationID(*locationId)
|
||||||
|
}
|
||||||
|
ttl := time.Minute * 15
|
||||||
|
endTime := time.Now().Add(ttl)
|
||||||
|
err := c.reservationService.ReserveForCart(ctx, inventory.CartReserveRequest{
|
||||||
|
CartID: inventory.CartID(cartId.String()),
|
||||||
|
InventoryReference: &inventory.InventoryReference{
|
||||||
|
SKU: inventory.SKU(sku),
|
||||||
|
LocationID: l,
|
||||||
|
},
|
||||||
|
TTL: ttl,
|
||||||
|
Quantity: uint32(quantity),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &endTime, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CartMutationContext) UseReservations(item *CartItem) bool {
|
||||||
|
if item.ReservationEndTime != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return item.Cgm == "55010"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CartMutationContext) ReleaseItem(ctx context.Context, cartId CartId, sku string, locationId *string) error {
|
||||||
|
if c.reservationService == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
l := inventory.LocationID("se")
|
||||||
|
if locationId != nil {
|
||||||
|
l = inventory.LocationID(*locationId)
|
||||||
|
}
|
||||||
|
return c.reservationService.ReleaseForCart(ctx, inventory.SKU(sku), l, inventory.CartID(cartId.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCartMultationRegistry(context *CartMutationContext) actor.MutationRegistry {
|
||||||
|
|
||||||
reg := actor.NewMutationRegistry()
|
reg := actor.NewMutationRegistry()
|
||||||
reg.RegisterMutations(
|
reg.RegisterMutations(
|
||||||
actor.NewMutation(AddItem, func() *messages.AddItem {
|
actor.NewMutation(context.AddItem),
|
||||||
return &messages.AddItem{}
|
actor.NewMutation(context.ChangeQuantity),
|
||||||
}),
|
actor.NewMutation(context.RemoveItem),
|
||||||
actor.NewMutation(ChangeQuantity, func() *messages.ChangeQuantity {
|
actor.NewMutation(ClearCart),
|
||||||
return &messages.ChangeQuantity{}
|
actor.NewMutation(AddVoucher),
|
||||||
}),
|
actor.NewMutation(RemoveVoucher),
|
||||||
actor.NewMutation(RemoveItem, func() *messages.RemoveItem {
|
actor.NewMutation(UpsertSubscriptionDetails),
|
||||||
return &messages.RemoveItem{}
|
actor.NewMutation(SetUserId),
|
||||||
}),
|
actor.NewMutation(LineItemMarking),
|
||||||
actor.NewMutation(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(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(PaymentDeclined, func() *messages.PaymentDeclined {
|
|
||||||
return &messages.PaymentDeclined{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(ConfirmationViewed, func() *messages.ConfirmationViewed {
|
|
||||||
return &messages.ConfirmationViewed{}
|
|
||||||
}),
|
|
||||||
actor.NewMutation(CreateCheckoutOrder, func() *messages.CreateCheckoutOrder {
|
|
||||||
return &messages.CreateCheckoutOrder{}
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+3
-1
@@ -4,6 +4,8 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||||
)
|
)
|
||||||
|
|
||||||
// cart_id.go
|
// cart_id.go
|
||||||
@@ -34,7 +36,7 @@ import (
|
|||||||
//
|
//
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
type CartId uint64
|
type CartId actor.GrainId
|
||||||
|
|
||||||
const base62Alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
const base62Alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
package cart
|
package cart
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
cart_messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mutation_add_item.go
|
// mutation_add_item.go
|
||||||
@@ -12,15 +17,17 @@ import (
|
|||||||
// This replaces the legacy switch-based logic previously found in CartGrain.Apply.
|
// This replaces the legacy switch-based logic previously found in CartGrain.Apply.
|
||||||
//
|
//
|
||||||
// Behavior:
|
// Behavior:
|
||||||
// * Validates quantity > 0
|
// - Validates quantity > 0
|
||||||
// * If an item with same SKU exists -> increases quantity
|
// - If an item with same SKU exists -> increases quantity
|
||||||
// * Else creates a new CartItem with computed tax amounts
|
// - Else creates a new CartItem with computed tax amounts
|
||||||
// * Totals recalculated automatically via WithTotals()
|
// - Totals recalculated automatically via WithTotals()
|
||||||
//
|
//
|
||||||
// NOTE: Any future field additions in messages.AddItem that affect pricing / tax
|
// NOTE: Any future field additions in messages.AddItem that affect pricing / tax
|
||||||
// must keep this handler in sync.
|
// must keep this handler in sync.
|
||||||
|
var ErrPaymentInProgress = errors.New("payment in progress")
|
||||||
|
|
||||||
func AddItem(g *CartGrain, m *messages.AddItem) error {
|
func (c *CartMutationContext) AddItem(g *CartGrain, m *cart_messages.AddItem) error {
|
||||||
|
ctx := context.Background()
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return fmt.Errorf("AddItem: nil payload")
|
return fmt.Errorf("AddItem: nil payload")
|
||||||
}
|
}
|
||||||
@@ -38,12 +45,23 @@ func AddItem(g *CartGrain, m *messages.AddItem) error {
|
|||||||
if !sameStore {
|
if !sameStore {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
existing.Quantity += int(m.Quantity)
|
if c.UseReservations(existing) {
|
||||||
existing.Stock = StockStatus(m.Stock)
|
if err := c.ReleaseItem(ctx, g.Id, existing.Sku, existing.StoreId); err != nil {
|
||||||
|
log.Printf("failed to release item %d: %v", existing.Id, err)
|
||||||
|
}
|
||||||
|
endTime, err := c.ReserveItem(ctx, g.Id, existing.Sku, existing.StoreId, existing.Quantity+uint16(m.Quantity))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
existing.ReservationEndTime = endTime
|
||||||
|
}
|
||||||
|
existing.Quantity += uint16(m.Quantity)
|
||||||
|
existing.Stock = uint16(m.Stock)
|
||||||
// If existing had nil store but new has one, adopt it.
|
// If existing had nil store but new has one, adopt it.
|
||||||
if existing.StoreId == nil && m.StoreId != nil {
|
if existing.StoreId == nil && m.StoreId != nil {
|
||||||
existing.StoreId = m.StoreId
|
existing.StoreId = m.StoreId
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,10 +76,15 @@ func AddItem(g *CartGrain, m *messages.AddItem) error {
|
|||||||
|
|
||||||
pricePerItem := NewPriceFromIncVat(m.Price, taxRate)
|
pricePerItem := NewPriceFromIncVat(m.Price, taxRate)
|
||||||
|
|
||||||
g.Items = append(g.Items, &CartItem{
|
needsReservation := true
|
||||||
|
if m.ReservationEndTime != nil {
|
||||||
|
needsReservation = m.ReservationEndTime.AsTime().Before(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
cartItem := &CartItem{
|
||||||
Id: g.lastItemId,
|
Id: g.lastItemId,
|
||||||
ItemId: uint32(m.ItemId),
|
ItemId: uint32(m.ItemId),
|
||||||
Quantity: int(m.Quantity),
|
Quantity: uint16(m.Quantity),
|
||||||
Sku: m.Sku,
|
Sku: m.Sku,
|
||||||
Tax: int(taxRate * 100),
|
Tax: int(taxRate * 100),
|
||||||
Meta: &ItemMeta{
|
Meta: &ItemMeta{
|
||||||
@@ -74,24 +97,38 @@ func AddItem(g *CartGrain, m *messages.AddItem) error {
|
|||||||
Category4: m.Category4,
|
Category4: m.Category4,
|
||||||
Category5: m.Category5,
|
Category5: m.Category5,
|
||||||
Outlet: m.Outlet,
|
Outlet: m.Outlet,
|
||||||
SellerId: m.SellerId,
|
|
||||||
SellerName: m.SellerName,
|
SellerName: m.SellerName,
|
||||||
Cgm: m.Cgm,
|
|
||||||
},
|
},
|
||||||
|
SellerId: m.SellerId,
|
||||||
|
Cgm: m.Cgm,
|
||||||
SaleStatus: m.SaleStatus,
|
SaleStatus: m.SaleStatus,
|
||||||
ParentId: m.ParentId,
|
ParentId: m.ParentId,
|
||||||
|
|
||||||
Price: *pricePerItem,
|
Price: *pricePerItem,
|
||||||
TotalPrice: *MultiplyPrice(*pricePerItem, int64(m.Quantity)),
|
TotalPrice: *MultiplyPrice(*pricePerItem, int64(m.Quantity)),
|
||||||
|
|
||||||
Stock: StockStatus(m.Stock),
|
Stock: uint16(m.Stock),
|
||||||
Disclaimer: m.Disclaimer,
|
Disclaimer: m.Disclaimer,
|
||||||
|
|
||||||
OrgPrice: getOrgPrice(m.OrgPrice, taxRate),
|
OrgPrice: getOrgPrice(m.OrgPrice, taxRate),
|
||||||
ArticleType: m.ArticleType,
|
ArticleType: m.ArticleType,
|
||||||
|
|
||||||
StoreId: m.StoreId,
|
StoreId: m.StoreId,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if needsReservation && c.UseReservations(cartItem) {
|
||||||
|
endTime, err := c.ReserveItem(ctx, g.Id, m.Sku, m.StoreId, uint16(m.Quantity))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if endTime != nil {
|
||||||
|
m.ReservationEndTime = timestamppb.New(*endTime)
|
||||||
|
t := m.ReservationEndTime.AsTime()
|
||||||
|
cartItem.ReservationEndTime = &t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Items = append(g.Items, cartItem)
|
||||||
g.UpdateTotals()
|
g.UpdateTotals()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package cart
|
|||||||
import (
|
import (
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"git.tornberg.me/go-cart-actor/pkg/actor"
|
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||||
"git.tornberg.me/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,6 +15,9 @@ func RemoveVoucher(g *CartGrain, m *messages.RemoveVoucher) error {
|
|||||||
StatusCode: 400,
|
StatusCode: 400,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if g.CheckoutStatus != nil {
|
||||||
|
return ErrPaymentInProgress
|
||||||
|
}
|
||||||
|
|
||||||
if !slices.ContainsFunc(g.Vouchers, func(v *Voucher) bool {
|
if !slices.ContainsFunc(g.Vouchers, func(v *Voucher) bool {
|
||||||
return v.Id == m.Id
|
return v.Id == m.Id
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package cart
|
package cart
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mutation_change_quantity.go
|
// mutation_change_quantity.go
|
||||||
@@ -26,11 +29,13 @@ import (
|
|||||||
// (If strict locking is required around every mutation, wrap logic in
|
// (If strict locking is required around every mutation, wrap logic in
|
||||||
// an explicit g.mu.Lock()/Unlock(), but current model mirrors prior code.)
|
// an explicit g.mu.Lock()/Unlock(), but current model mirrors prior code.)
|
||||||
|
|
||||||
func ChangeQuantity(g *CartGrain, m *messages.ChangeQuantity) error {
|
func (c *CartMutationContext) ChangeQuantity(g *CartGrain, m *messages.ChangeQuantity) error {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return fmt.Errorf("ChangeQuantity: nil payload")
|
return fmt.Errorf("ChangeQuantity: nil payload")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
foundIndex := -1
|
foundIndex := -1
|
||||||
for i, it := range g.Items {
|
for i, it := range g.Items {
|
||||||
if it.Id == uint32(m.Id) {
|
if it.Id == uint32(m.Id) {
|
||||||
@@ -44,12 +49,37 @@ func ChangeQuantity(g *CartGrain, m *messages.ChangeQuantity) error {
|
|||||||
|
|
||||||
if m.Quantity <= 0 {
|
if m.Quantity <= 0 {
|
||||||
// Remove the item
|
// Remove the item
|
||||||
|
itemToRemove := g.Items[foundIndex]
|
||||||
|
if itemToRemove.ReservationEndTime != nil && itemToRemove.ReservationEndTime.Before(time.Now()) {
|
||||||
|
err := c.ReleaseItem(ctx, g.Id, itemToRemove.Sku, itemToRemove.StoreId)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("unable to release reservation for %s in location: %v", itemToRemove.Sku, itemToRemove.StoreId)
|
||||||
|
}
|
||||||
|
}
|
||||||
g.Items = append(g.Items[:foundIndex], g.Items[foundIndex+1:]...)
|
g.Items = append(g.Items[:foundIndex], g.Items[foundIndex+1:]...)
|
||||||
g.UpdateTotals()
|
g.UpdateTotals()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
item := g.Items[foundIndex]
|
||||||
|
if item == nil {
|
||||||
|
return fmt.Errorf("ChangeQuantity: item id %d not found", m.Id)
|
||||||
|
}
|
||||||
|
if c.UseReservations(item) {
|
||||||
|
if item.ReservationEndTime != nil {
|
||||||
|
err := c.ReleaseItem(ctx, g.Id, item.Sku, item.StoreId)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("unable to release reservation for %s in location: %v", item.Sku, item.StoreId)
|
||||||
|
}
|
||||||
|
|
||||||
g.Items[foundIndex].Quantity = int(m.Quantity)
|
}
|
||||||
|
endTime, err := c.ReserveItem(ctx, g.Id, item.Sku, item.StoreId, uint16(m.Quantity))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
item.ReservationEndTime = endTime
|
||||||
|
}
|
||||||
|
item.Quantity = uint16(m.Quantity)
|
||||||
g.UpdateTotals()
|
g.UpdateTotals()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.tornberg.me/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"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
messages "git.tornberg.me/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,44 +0,0 @@
|
|||||||
package cart
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
messages "git.tornberg.me/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 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")
|
|
||||||
}
|
|
||||||
|
|
||||||
g.OrderReference = m.OrderId
|
|
||||||
g.PaymentStatus = m.Status
|
|
||||||
g.PaymentInProgress = m.PaymentInProgress
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package cart
|
|
||||||
|
|
||||||
import "git.tornberg.me/go-cart-actor/pkg/messages"
|
|
||||||
|
|
||||||
func InventoryReserved(g *CartGrain, m *messages.InventoryReserved) error {
|
|
||||||
g.InventoryReserved = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ package cart
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
messages "git.tornberg.me/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,21 +0,0 @@
|
|||||||
package cart
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
|
||||||
)
|
|
||||||
|
|
||||||
func PaymentDeclined(grain *CartGrain, req *messages.PaymentDeclined) error {
|
|
||||||
grain.PaymentStatus = "declined"
|
|
||||||
grain.PaymentDeclinedNotices = append(grain.PaymentDeclinedNotices, Notice{
|
|
||||||
Timestamp: time.Now(),
|
|
||||||
Message: req.Message,
|
|
||||||
Code: req.Code,
|
|
||||||
})
|
|
||||||
// Optionally clear checkout order if in progress
|
|
||||||
if grain.CheckoutOrderId != "" {
|
|
||||||
grain.CheckoutOrderId = ""
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package cart
|
|
||||||
|
|
||||||
import messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
|
||||||
|
|
||||||
func PreConditionFailed(g *CartGrain, m *messages.PreConditionFailed) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
package cart
|
package cart
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mutation_remove_item.go
|
// mutation_remove_item.go
|
||||||
@@ -22,10 +25,11 @@ import (
|
|||||||
// - If multiple lines somehow shared the same Id (should not happen), only
|
// - If multiple lines somehow shared the same Id (should not happen), only
|
||||||
// the first match would be removed—data integrity relies on unique line Ids.
|
// the first match would be removed—data integrity relies on unique line Ids.
|
||||||
|
|
||||||
func RemoveItem(g *CartGrain, m *messages.RemoveItem) error {
|
func (c *CartMutationContext) RemoveItem(g *CartGrain, m *messages.RemoveItem) error {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return fmt.Errorf("RemoveItem: nil payload")
|
return fmt.Errorf("RemoveItem: nil payload")
|
||||||
}
|
}
|
||||||
|
|
||||||
targetID := uint32(m.Id)
|
targetID := uint32(m.Id)
|
||||||
|
|
||||||
index := -1
|
index := -1
|
||||||
@@ -39,6 +43,14 @@ func RemoveItem(g *CartGrain, m *messages.RemoveItem) error {
|
|||||||
return fmt.Errorf("RemoveItem: item id %d not found", m.Id)
|
return fmt.Errorf("RemoveItem: item id %d not found", m.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item := g.Items[index]
|
||||||
|
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.Items = append(g.Items[:index], g.Items[index+1:]...)
|
g.Items = append(g.Items[:index], g.Items[index+1:]...)
|
||||||
g.UpdateTotals()
|
g.UpdateTotals()
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ package cart
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
messages "git.tornberg.me/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 {
|
||||||
|
|
||||||
for i, item := range grain.Items {
|
for i, item := range grain.Items {
|
||||||
if item.Id == req.Id {
|
if item.Id == req.Id {
|
||||||
grain.Items[i].Marking = nil
|
grain.Items[i].Marking = nil
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
package cart
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"slices"
|
|
||||||
|
|
||||||
messages "git.tornberg.me/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")
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,8 @@ package cart
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
messages "git.tornberg.me/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 {
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ package cart
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
messages "git.tornberg.me/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 {
|
||||||
|
|
||||||
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,719 +0,0 @@
|
|||||||
package cart
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gogo/protobuf/proto"
|
|
||||||
"google.golang.org/protobuf/types/known/anypb"
|
|
||||||
|
|
||||||
"git.tornberg.me/go-cart-actor/pkg/actor"
|
|
||||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ----------------------
|
|
||||||
// Helper constructors
|
|
||||||
// ----------------------
|
|
||||||
|
|
||||||
func newTestGrain() *CartGrain {
|
|
||||||
return NewCartGrain(123, time.Now())
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRegistry() actor.MutationRegistry {
|
|
||||||
return NewCartMultationRegistry()
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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",
|
|
||||||
}
|
|
||||||
|
|
||||||
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 -> result should contain ErrMutationNotRegistered, no top-level error
|
|
||||||
results, err := reg.Apply(context.Background(), newTestGrain(), &messages.Noop{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected top-level error applying unregistered mutation: %v", err)
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
|
|
||||||
g.CheckoutOrderId = "test-order"
|
|
||||||
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 %d", 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")
|
|
||||||
}
|
|
||||||
@@ -4,13 +4,14 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
messages "git.tornberg.me/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
|
||||||
}
|
}
|
||||||
|
|
||||||
metaBytes := m.Data.GetValue()
|
metaBytes := m.Data.GetValue()
|
||||||
|
|
||||||
// Create new subscription details when Id is nil.
|
// Create new subscription details when Id is nil.
|
||||||
|
|||||||
@@ -87,6 +87,34 @@ func (p Price) MarshalJSON() ([]byte, error) {
|
|||||||
return json.Marshal(wire{ExVat: exVat, IncVat: p.IncVat, Vat: vat})
|
return json.Marshal(wire{ExVat: exVat, IncVat: p.IncVat, Vat: vat})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Price) UnmarshalJSON(data []byte) error {
|
||||||
|
type wire struct {
|
||||||
|
ExVat int64 `json:"exVat"`
|
||||||
|
IncVat int64 `json:"incVat"`
|
||||||
|
Vat map[string]int64 `json:"vat,omitempty"`
|
||||||
|
}
|
||||||
|
var w wire
|
||||||
|
if err := json.Unmarshal(data, &w); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.IncVat = w.IncVat
|
||||||
|
if len(w.Vat) > 0 {
|
||||||
|
p.VatRates = make(map[float32]int64, len(w.Vat))
|
||||||
|
for rateStr, amount := range w.Vat {
|
||||||
|
rate, err := strconv.ParseFloat(rateStr, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.VatRates[float32(rate)] = amount
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p.VatRates = make(map[float32]int64)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// trimFloat converts a float32 tax rate like 25 or 12.5 into a compact string without
|
// trimFloat converts a float32 tax rate like 25 or 12.5 into a compact string without
|
||||||
// unnecessary decimals ("25", "12.5").
|
// unnecessary decimals ("25", "12.5").
|
||||||
func trimFloat(f float32) string {
|
func trimFloat(f float32) string {
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
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"`
|
||||||
|
SessionData *json.RawMessage `json:"sessionData,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 ContactDetails struct {
|
||||||
|
Email *string `json:"email,omitempty"`
|
||||||
|
Phone *string `json:"phone,omitempty"`
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 `json:"version"`
|
||||||
|
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"`
|
||||||
|
ContactDetails *ContactDetails `json:"contactDetails,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCheckoutGrain(id uint64, cartId cart.CartId, cartVersion uint64, ts time.Time, cartState *cart.CartGrain) *CheckoutGrain {
|
||||||
|
r := &CheckoutGrain{
|
||||||
|
lastDeliveryId: 0,
|
||||||
|
lastGiftcardId: 0,
|
||||||
|
lastAccess: ts,
|
||||||
|
lastChange: ts,
|
||||||
|
Id: CheckoutId(id),
|
||||||
|
CartId: cartId,
|
||||||
|
CartVersion: cartVersion,
|
||||||
|
Deliveries: []*CheckoutDelivery{},
|
||||||
|
Payments: []*Payment{},
|
||||||
|
}
|
||||||
|
if cartState != nil {
|
||||||
|
r.CartState = cartState
|
||||||
|
r.CartTotalPrice = cartState.TotalPrice
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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),
|
||||||
|
actor.NewMutation(HandleContactDetailsUpdated),
|
||||||
|
)
|
||||||
|
return reg
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package checkout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
checkout_messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleContactDetailsUpdated mutation
|
||||||
|
func HandleContactDetailsUpdated(g *CheckoutGrain, m *checkout_messages.ContactDetailsUpdated) error {
|
||||||
|
if m == nil {
|
||||||
|
return fmt.Errorf("HandleContactDetailsUpdated: nil payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.ContactDetails == nil {
|
||||||
|
g.ContactDetails = &ContactDetails{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Email != nil {
|
||||||
|
g.ContactDetails.Email = m.Email
|
||||||
|
}
|
||||||
|
if m.Phone != nil {
|
||||||
|
g.ContactDetails.Phone = m.Phone
|
||||||
|
}
|
||||||
|
if m.Name != nil {
|
||||||
|
g.ContactDetails.Name = m.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package checkout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
checkout_messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandleContactDetailsUpdated(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
initial *ContactDetails
|
||||||
|
update *checkout_messages.ContactDetailsUpdated
|
||||||
|
expected *ContactDetails
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "update all fields",
|
||||||
|
initial: nil,
|
||||||
|
update: &checkout_messages.ContactDetailsUpdated{
|
||||||
|
Email: stringPtr("test@example.com"),
|
||||||
|
Phone: stringPtr("123456789"),
|
||||||
|
Name: stringPtr("John Doe"),
|
||||||
|
},
|
||||||
|
expected: &ContactDetails{
|
||||||
|
Email: stringPtr("test@example.com"),
|
||||||
|
Phone: stringPtr("123456789"),
|
||||||
|
Name: stringPtr("John Doe"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update partial fields",
|
||||||
|
initial: &ContactDetails{
|
||||||
|
Email: stringPtr("old@example.com"),
|
||||||
|
Phone: nil,
|
||||||
|
Name: stringPtr("Old Name"),
|
||||||
|
},
|
||||||
|
update: &checkout_messages.ContactDetailsUpdated{
|
||||||
|
Phone: stringPtr("987654321"),
|
||||||
|
},
|
||||||
|
expected: &ContactDetails{
|
||||||
|
Email: stringPtr("old@example.com"),
|
||||||
|
Phone: stringPtr("987654321"),
|
||||||
|
Name: stringPtr("Old Name"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil message",
|
||||||
|
initial: nil,
|
||||||
|
update: nil,
|
||||||
|
expected: &ContactDetails{}, // but error expected
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
grain := &CheckoutGrain{
|
||||||
|
ContactDetails: tt.initial,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := HandleContactDetailsUpdated(grain, tt.update)
|
||||||
|
|
||||||
|
if tt.name == "nil message" {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error for nil message, got nil")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if grain.ContactDetails == nil && tt.expected != nil {
|
||||||
|
t.Errorf("expected contact details, got nil")
|
||||||
|
} else if grain.ContactDetails != nil && tt.expected == nil {
|
||||||
|
t.Errorf("expected nil contact details, got %v", grain.ContactDetails)
|
||||||
|
} else if grain.ContactDetails != nil && tt.expected != nil {
|
||||||
|
if (grain.ContactDetails.Email == nil && tt.expected.Email != nil) || (grain.ContactDetails.Email != nil && tt.expected.Email == nil) || (grain.ContactDetails.Email != nil && *grain.ContactDetails.Email != *tt.expected.Email) {
|
||||||
|
t.Errorf("email mismatch: got %v, expected %v", grain.ContactDetails.Email, tt.expected.Email)
|
||||||
|
}
|
||||||
|
if (grain.ContactDetails.Phone == nil && tt.expected.Phone != nil) || (grain.ContactDetails.Phone != nil && tt.expected.Phone == nil) || (grain.ContactDetails.Phone != nil && *grain.ContactDetails.Phone != *tt.expected.Phone) {
|
||||||
|
t.Errorf("phone mismatch: got %v, expected %v", grain.ContactDetails.Phone, tt.expected.Phone)
|
||||||
|
}
|
||||||
|
if (grain.ContactDetails.Name == nil && tt.expected.Name != nil) || (grain.ContactDetails.Name != nil && tt.expected.Name == nil) || (grain.ContactDetails.Name != nil && *grain.ContactDetails.Name != *tt.expected.Name) {
|
||||||
|
t.Errorf("name mismatch: got %v, expected %v", grain.ContactDetails.Name, tt.expected.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringPtr(s string) *string {
|
||||||
|
return &s
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package checkout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
|
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 g.OrderId == "" {
|
||||||
|
// return fmt.Errorf("InitializeCheckout: missing orderId")
|
||||||
|
// }
|
||||||
|
if g.CartState != nil {
|
||||||
|
if g.CartId != cart.CartId(m.CartId) {
|
||||||
|
return fmt.Errorf("InitializeCheckout: cart ID mismatch")
|
||||||
|
}
|
||||||
|
if g.PaymentInProgress > 0 || g.OrderId != nil {
|
||||||
|
return fmt.Errorf("InitializeCheckout: payment already in progress")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal(m.CartState.Value, &g.CartState)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
g.CartTotalPrice = g.CartState.TotalPrice
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -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.tornberg.me/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
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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
|
||||||
|
if m.Amount > 0 {
|
||||||
|
payment.Amount = m.Amount
|
||||||
|
}
|
||||||
|
if m.Currency != "" {
|
||||||
|
payment.Currency = m.Currency
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.CompletedAt != nil {
|
||||||
|
*payment.CompletedAt = m.CompletedAt.AsTime()
|
||||||
|
} else {
|
||||||
|
*payment.CompletedAt = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update checkout status
|
||||||
|
g.PaymentInProgress--
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package checkout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
checkout_messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||||
|
"google.golang.org/protobuf/types/known/anypb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandlePaymentEvent(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
initialPayments []*Payment
|
||||||
|
event *checkout_messages.PaymentEvent
|
||||||
|
expectedEvents []PaymentEvent
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "add event to existing payment",
|
||||||
|
initialPayments: []*Payment{
|
||||||
|
{
|
||||||
|
PaymentId: "pay123",
|
||||||
|
Events: []PaymentEvent{
|
||||||
|
{Name: "init", Success: true, Data: json.RawMessage(`{"key":"value"}`)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
event: &checkout_messages.PaymentEvent{
|
||||||
|
PaymentId: "pay123",
|
||||||
|
Name: "capture",
|
||||||
|
Success: true,
|
||||||
|
Data: &anypb.Any{Value: []byte(`{"amount":100}`)},
|
||||||
|
},
|
||||||
|
expectedEvents: []PaymentEvent{
|
||||||
|
{Name: "init", Success: true, Data: json.RawMessage(`{"key":"value"}`)},
|
||||||
|
{Name: "capture", Success: true, Data: json.RawMessage(`{"amount":100}`)},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add event to payment with no initial events",
|
||||||
|
initialPayments: []*Payment{
|
||||||
|
{
|
||||||
|
PaymentId: "pay456",
|
||||||
|
Events: []PaymentEvent{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
event: &checkout_messages.PaymentEvent{
|
||||||
|
PaymentId: "pay456",
|
||||||
|
Name: "refund",
|
||||||
|
Success: false,
|
||||||
|
Data: &anypb.Any{Value: []byte(`{"reason":"failed"}`)},
|
||||||
|
},
|
||||||
|
expectedEvents: []PaymentEvent{
|
||||||
|
{Name: "refund", Success: false, Data: json.RawMessage(`{"reason":"failed"}`)},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "payment not found",
|
||||||
|
initialPayments: []*Payment{
|
||||||
|
{
|
||||||
|
PaymentId: "pay123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
event: &checkout_messages.PaymentEvent{
|
||||||
|
PaymentId: "nonexistent",
|
||||||
|
Name: "test",
|
||||||
|
Success: true,
|
||||||
|
},
|
||||||
|
expectedEvents: nil,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
grain := &CheckoutGrain{
|
||||||
|
Payments: tt.initialPayments,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := HandlePaymentEvent(grain, tt.event)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error, got nil")
|
||||||
|
} else if err != ErrPaymentNotFound {
|
||||||
|
t.Errorf("expected ErrPaymentNotFound, got %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
payment, found := grain.FindPayment(tt.event.PaymentId)
|
||||||
|
if !found {
|
||||||
|
t.Errorf("payment not found after handling event")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(payment.Events) != len(tt.expectedEvents) {
|
||||||
|
t.Errorf("expected %d events, got %d", len(tt.expectedEvents), len(payment.Events))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, expected := range tt.expectedEvents {
|
||||||
|
actual := payment.Events[i]
|
||||||
|
if actual.Name != expected.Name {
|
||||||
|
t.Errorf("event %d name mismatch: got %s, expected %s", i, actual.Name, expected.Name)
|
||||||
|
}
|
||||||
|
if actual.Success != expected.Success {
|
||||||
|
t.Errorf("event %d success mismatch: got %t, expected %t", i, actual.Success, expected.Success)
|
||||||
|
}
|
||||||
|
if string(actual.Data) != string(expected.Data) {
|
||||||
|
t.Errorf("event %d data mismatch: got %s, expected %s", i, string(actual.Data), string(expected.Data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package checkout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
// marks the checkout as having a payment in progress.
|
||||||
|
func HandlePaymentStarted(g *CheckoutGrain, m *messages.PaymentStarted) error {
|
||||||
|
if m == nil {
|
||||||
|
return fmt.Errorf("PaymentStarted: nil payload")
|
||||||
|
}
|
||||||
|
paymentID := strings.TrimSpace(m.PaymentId)
|
||||||
|
if paymentID == "" {
|
||||||
|
return fmt.Errorf("PaymentStarted: missing paymentId")
|
||||||
|
}
|
||||||
|
if m.Amount < 0 {
|
||||||
|
return fmt.Errorf("PaymentStarted: amount cannot be negative")
|
||||||
|
}
|
||||||
|
|
||||||
|
currency := strings.TrimSpace(m.Currency)
|
||||||
|
provider := strings.TrimSpace(m.Provider)
|
||||||
|
method := copyOptionalString(m.Method)
|
||||||
|
|
||||||
|
startedAt := time.Now().UTC()
|
||||||
|
if m.StartedAt != nil {
|
||||||
|
startedAt = m.StartedAt.AsTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
payment, found := g.FindPayment(paymentID)
|
||||||
|
|
||||||
|
var sessionData *json.RawMessage
|
||||||
|
if m.SessionData != nil {
|
||||||
|
sessionData = (*json.RawMessage)(&m.SessionData.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if found {
|
||||||
|
if payment.Status != "pending" {
|
||||||
|
return fmt.Errorf("PaymentStarted: payment already started")
|
||||||
|
}
|
||||||
|
if payment.PaymentId != paymentID {
|
||||||
|
payment.PaymentId = paymentID
|
||||||
|
}
|
||||||
|
payment.Status = "pending"
|
||||||
|
payment.Amount = m.Amount
|
||||||
|
if currency != "" {
|
||||||
|
payment.Currency = currency
|
||||||
|
}
|
||||||
|
if provider != "" {
|
||||||
|
payment.Provider = provider
|
||||||
|
}
|
||||||
|
if method != nil {
|
||||||
|
payment.Method = method
|
||||||
|
}
|
||||||
|
if m.SessionData != nil {
|
||||||
|
payment.SessionData = sessionData
|
||||||
|
}
|
||||||
|
payment.StartedAt = &startedAt
|
||||||
|
|
||||||
|
} else {
|
||||||
|
g.PaymentInProgress++
|
||||||
|
g.Payments = append(g.Payments, &Payment{
|
||||||
|
PaymentId: paymentID,
|
||||||
|
Status: "pending",
|
||||||
|
Amount: m.Amount,
|
||||||
|
SessionData: sessionData,
|
||||||
|
Currency: currency,
|
||||||
|
Provider: provider,
|
||||||
|
Method: method,
|
||||||
|
StartedAt: &startedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyOptionalString(src *string) *string {
|
||||||
|
if src == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
trimmed := strings.TrimSpace(*src)
|
||||||
|
if trimmed == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dst := trimmed
|
||||||
|
return &dst
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package cart
|
package checkout
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mutation_remove_delivery.go
|
// mutation_remove_delivery.go
|
||||||
@@ -13,20 +13,10 @@ 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")
|
||||||
}
|
}
|
||||||
@@ -44,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.tornberg.me/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,34 +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")
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
+19
-17
@@ -14,36 +14,39 @@ 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
|
||||||
}
|
}
|
||||||
hosts := make([]string, 0, len(pods.Items))
|
hosts := make([]string, 0, len(pods.Items))
|
||||||
for _, pod := range pods.Items {
|
for _, pod := range pods.Items {
|
||||||
hosts = append(hosts, pod.Status.PodIP)
|
if hasReadyCondition(&pod) {
|
||||||
|
hosts = append(hosts, pod.Status.PodIP)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return hosts, nil
|
return hosts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasReadyCondition(pod *v1.Pod) bool {
|
||||||
|
return slices.ContainsFunc(pod.Status.Conditions, func(condition v1.PodCondition) bool {
|
||||||
|
return condition.Type == v1.PodReady && condition.Status == v1.ConditionTrue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -54,9 +57,7 @@ func (k *K8sDiscovery) Watch() (<-chan HostChange, error) {
|
|||||||
for event := range watcher.ResultChan() {
|
for event := range watcher.ResultChan() {
|
||||||
|
|
||||||
pod := event.Object.(*v1.Pod)
|
pod := event.Object.(*v1.Pod)
|
||||||
isReady := slices.ContainsFunc(pod.Status.Conditions, func(condition v1.PodCondition) bool {
|
isReady := hasReadyCondition(pod)
|
||||||
return condition.Type == v1.PodReady && condition.Status == v1.ConditionTrue
|
|
||||||
})
|
|
||||||
m.Lock()
|
m.Lock()
|
||||||
oldState := ipsThatAreReady[pod.Status.PodIP]
|
oldState := ipsThatAreReady[pod.Status.PodIP]
|
||||||
ipsThatAreReady[pod.Status.PodIP] = isReady
|
ipsThatAreReady[pod.Status.PodIP] = isReady
|
||||||
@@ -76,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)
|
||||||
|
|||||||
@@ -9,3 +9,9 @@ type Discovery interface {
|
|||||||
Discover() ([]string, error)
|
Discover() ([]string, error)
|
||||||
Watch() (<-chan HostChange, error)
|
Watch() (<-chan HostChange, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DiscoveryTarget interface {
|
||||||
|
IsKnown(string) bool
|
||||||
|
RemoveHost(host string)
|
||||||
|
AddRemoteHost(host string)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,582 +0,0 @@
|
|||||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// protoc-gen-go v1.36.5
|
|
||||||
// protoc v6.33.1
|
|
||||||
// source: control_plane.proto
|
|
||||||
|
|
||||||
package messages
|
|
||||||
|
|
||||||
import (
|
|
||||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
|
||||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
|
||||||
reflect "reflect"
|
|
||||||
sync "sync"
|
|
||||||
unsafe "unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Verify that this generated code is sufficiently up-to-date.
|
|
||||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
|
||||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
|
||||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Empty request placeholder (common pattern).
|
|
||||||
type Empty struct {
|
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *Empty) Reset() {
|
|
||||||
*x = Empty{}
|
|
||||||
mi := &file_control_plane_proto_msgTypes[0]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *Empty) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*Empty) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *Empty) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_control_plane_proto_msgTypes[0]
|
|
||||||
if x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use Empty.ProtoReflect.Descriptor instead.
|
|
||||||
func (*Empty) Descriptor() ([]byte, []int) {
|
|
||||||
return file_control_plane_proto_rawDescGZIP(), []int{0}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ping reply includes responding host and its current unix time (seconds).
|
|
||||||
type PingReply struct {
|
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
|
||||||
Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"`
|
|
||||||
UnixTime int64 `protobuf:"varint,2,opt,name=unix_time,json=unixTime,proto3" json:"unix_time,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *PingReply) Reset() {
|
|
||||||
*x = PingReply{}
|
|
||||||
mi := &file_control_plane_proto_msgTypes[1]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *PingReply) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*PingReply) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *PingReply) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_control_plane_proto_msgTypes[1]
|
|
||||||
if x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use PingReply.ProtoReflect.Descriptor instead.
|
|
||||||
func (*PingReply) Descriptor() ([]byte, []int) {
|
|
||||||
return file_control_plane_proto_rawDescGZIP(), []int{1}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *PingReply) GetHost() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.Host
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *PingReply) GetUnixTime() int64 {
|
|
||||||
if x != nil {
|
|
||||||
return x.UnixTime
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// NegotiateRequest carries the caller's full view of known hosts (including self).
|
|
||||||
type NegotiateRequest struct {
|
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
|
||||||
KnownHosts []string `protobuf:"bytes,1,rep,name=known_hosts,json=knownHosts,proto3" json:"known_hosts,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NegotiateRequest) Reset() {
|
|
||||||
*x = NegotiateRequest{}
|
|
||||||
mi := &file_control_plane_proto_msgTypes[2]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NegotiateRequest) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*NegotiateRequest) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *NegotiateRequest) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_control_plane_proto_msgTypes[2]
|
|
||||||
if x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use NegotiateRequest.ProtoReflect.Descriptor instead.
|
|
||||||
func (*NegotiateRequest) Descriptor() ([]byte, []int) {
|
|
||||||
return file_control_plane_proto_rawDescGZIP(), []int{2}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NegotiateRequest) GetKnownHosts() []string {
|
|
||||||
if x != nil {
|
|
||||||
return x.KnownHosts
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NegotiateReply returns the callee's healthy hosts (including itself).
|
|
||||||
type NegotiateReply struct {
|
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
|
||||||
Hosts []string `protobuf:"bytes,1,rep,name=hosts,proto3" json:"hosts,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NegotiateReply) Reset() {
|
|
||||||
*x = NegotiateReply{}
|
|
||||||
mi := &file_control_plane_proto_msgTypes[3]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NegotiateReply) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*NegotiateReply) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *NegotiateReply) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_control_plane_proto_msgTypes[3]
|
|
||||||
if x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use NegotiateReply.ProtoReflect.Descriptor instead.
|
|
||||||
func (*NegotiateReply) Descriptor() ([]byte, []int) {
|
|
||||||
return file_control_plane_proto_rawDescGZIP(), []int{3}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *NegotiateReply) GetHosts() []string {
|
|
||||||
if x != nil {
|
|
||||||
return x.Hosts
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CartIdsReply returns the list of cart IDs (string form) currently owned locally.
|
|
||||||
type ActorIdsReply struct {
|
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
|
||||||
Ids []uint64 `protobuf:"varint,1,rep,packed,name=ids,proto3" json:"ids,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *ActorIdsReply) Reset() {
|
|
||||||
*x = ActorIdsReply{}
|
|
||||||
mi := &file_control_plane_proto_msgTypes[4]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *ActorIdsReply) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*ActorIdsReply) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *ActorIdsReply) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_control_plane_proto_msgTypes[4]
|
|
||||||
if x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use ActorIdsReply.ProtoReflect.Descriptor instead.
|
|
||||||
func (*ActorIdsReply) Descriptor() ([]byte, []int) {
|
|
||||||
return file_control_plane_proto_rawDescGZIP(), []int{4}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *ActorIdsReply) GetIds() []uint64 {
|
|
||||||
if x != nil {
|
|
||||||
return x.Ids
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// OwnerChangeAck retained as response type for Closing RPC (ConfirmOwner removed).
|
|
||||||
type OwnerChangeAck struct {
|
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
|
||||||
Accepted bool `protobuf:"varint,1,opt,name=accepted,proto3" json:"accepted,omitempty"`
|
|
||||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *OwnerChangeAck) Reset() {
|
|
||||||
*x = OwnerChangeAck{}
|
|
||||||
mi := &file_control_plane_proto_msgTypes[5]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *OwnerChangeAck) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*OwnerChangeAck) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *OwnerChangeAck) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_control_plane_proto_msgTypes[5]
|
|
||||||
if x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use OwnerChangeAck.ProtoReflect.Descriptor instead.
|
|
||||||
func (*OwnerChangeAck) Descriptor() ([]byte, []int) {
|
|
||||||
return file_control_plane_proto_rawDescGZIP(), []int{5}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *OwnerChangeAck) GetAccepted() bool {
|
|
||||||
if x != nil {
|
|
||||||
return x.Accepted
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *OwnerChangeAck) GetMessage() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.Message
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClosingNotice notifies peers this host is terminating (so they can drop / re-resolve).
|
|
||||||
type ClosingNotice struct {
|
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
|
||||||
Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *ClosingNotice) Reset() {
|
|
||||||
*x = ClosingNotice{}
|
|
||||||
mi := &file_control_plane_proto_msgTypes[6]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *ClosingNotice) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*ClosingNotice) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *ClosingNotice) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_control_plane_proto_msgTypes[6]
|
|
||||||
if x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use ClosingNotice.ProtoReflect.Descriptor instead.
|
|
||||||
func (*ClosingNotice) Descriptor() ([]byte, []int) {
|
|
||||||
return file_control_plane_proto_rawDescGZIP(), []int{6}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *ClosingNotice) GetHost() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.Host
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// OwnershipAnnounce broadcasts first-touch ownership claims for cart IDs.
|
|
||||||
// First claim wins; receivers SHOULD NOT overwrite an existing different owner.
|
|
||||||
type OwnershipAnnounce struct {
|
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
|
||||||
Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` // announcing host
|
|
||||||
Ids []uint64 `protobuf:"varint,2,rep,packed,name=ids,proto3" json:"ids,omitempty"` // newly claimed cart ids
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *OwnershipAnnounce) Reset() {
|
|
||||||
*x = OwnershipAnnounce{}
|
|
||||||
mi := &file_control_plane_proto_msgTypes[7]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *OwnershipAnnounce) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*OwnershipAnnounce) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *OwnershipAnnounce) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_control_plane_proto_msgTypes[7]
|
|
||||||
if x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use OwnershipAnnounce.ProtoReflect.Descriptor instead.
|
|
||||||
func (*OwnershipAnnounce) Descriptor() ([]byte, []int) {
|
|
||||||
return file_control_plane_proto_rawDescGZIP(), []int{7}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *OwnershipAnnounce) GetHost() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.Host
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *OwnershipAnnounce) GetIds() []uint64 {
|
|
||||||
if x != nil {
|
|
||||||
return x.Ids
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExpiryAnnounce broadcasts that a host evicted the provided cart IDs.
|
|
||||||
type ExpiryAnnounce struct {
|
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
|
||||||
Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"`
|
|
||||||
Ids []uint64 `protobuf:"varint,2,rep,packed,name=ids,proto3" json:"ids,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *ExpiryAnnounce) Reset() {
|
|
||||||
*x = ExpiryAnnounce{}
|
|
||||||
mi := &file_control_plane_proto_msgTypes[8]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *ExpiryAnnounce) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*ExpiryAnnounce) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *ExpiryAnnounce) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_control_plane_proto_msgTypes[8]
|
|
||||||
if x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use ExpiryAnnounce.ProtoReflect.Descriptor instead.
|
|
||||||
func (*ExpiryAnnounce) Descriptor() ([]byte, []int) {
|
|
||||||
return file_control_plane_proto_rawDescGZIP(), []int{8}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *ExpiryAnnounce) GetHost() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.Host
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *ExpiryAnnounce) GetIds() []uint64 {
|
|
||||||
if x != nil {
|
|
||||||
return x.Ids
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var File_control_plane_proto protoreflect.FileDescriptor
|
|
||||||
|
|
||||||
var file_control_plane_proto_rawDesc = string([]byte{
|
|
||||||
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, 0x22,
|
|
||||||
0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x3c, 0x0a, 0x09, 0x50, 0x69, 0x6e, 0x67,
|
|
||||||
0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x01, 0x20,
|
|
||||||
0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x75, 0x6e, 0x69,
|
|
||||||
0x78, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x75, 0x6e,
|
|
||||||
0x69, 0x78, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x33, 0x0a, 0x10, 0x4e, 0x65, 0x67, 0x6f, 0x74, 0x69,
|
|
||||||
0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6b, 0x6e,
|
|
||||||
0x6f, 0x77, 0x6e, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52,
|
|
||||||
0x0a, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x22, 0x26, 0x0a, 0x0e, 0x4e,
|
|
||||||
0x65, 0x67, 0x6f, 0x74, 0x69, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x14, 0x0a,
|
|
||||||
0x05, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x68, 0x6f,
|
|
||||||
0x73, 0x74, 0x73, 0x22, 0x21, 0x0a, 0x0d, 0x41, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x73, 0x52,
|
|
||||||
0x65, 0x70, 0x6c, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28,
|
|
||||||
0x04, 0x52, 0x03, 0x69, 0x64, 0x73, 0x22, 0x46, 0x0a, 0x0e, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x43,
|
|
||||||
0x68, 0x61, 0x6e, 0x67, 0x65, 0x41, 0x63, 0x6b, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x65,
|
|
||||||
0x70, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x63, 0x63, 0x65,
|
|
||||||
0x70, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18,
|
|
||||||
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x23,
|
|
||||||
0x0a, 0x0d, 0x43, 0x6c, 0x6f, 0x73, 0x69, 0x6e, 0x67, 0x4e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x12,
|
|
||||||
0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68,
|
|
||||||
0x6f, 0x73, 0x74, 0x22, 0x39, 0x0a, 0x11, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70,
|
|
||||||
0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74,
|
|
||||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03,
|
|
||||||
0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x04, 0x52, 0x03, 0x69, 0x64, 0x73, 0x22, 0x36,
|
|
||||||
0x0a, 0x0e, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65,
|
|
||||||
0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
|
||||||
0x68, 0x6f, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
|
|
||||||
0x04, 0x52, 0x03, 0x69, 0x64, 0x73, 0x32, 0x8d, 0x03, 0x0a, 0x0c, 0x43, 0x6f, 0x6e, 0x74, 0x72,
|
|
||||||
0x6f, 0x6c, 0x50, 0x6c, 0x61, 0x6e, 0x65, 0x12, 0x2c, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12,
|
|
||||||
0x0f, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79,
|
|
||||||
0x1a, 0x13, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x50, 0x69, 0x6e, 0x67,
|
|
||||||
0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x41, 0x0a, 0x09, 0x4e, 0x65, 0x67, 0x6f, 0x74, 0x69, 0x61,
|
|
||||||
0x74, 0x65, 0x12, 0x1a, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x4e, 0x65,
|
|
||||||
0x67, 0x6f, 0x74, 0x69, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18,
|
|
||||||
0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x4e, 0x65, 0x67, 0x6f, 0x74, 0x69,
|
|
||||||
0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x3c, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x4c,
|
|
||||||
0x6f, 0x63, 0x61, 0x6c, 0x41, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x73, 0x12, 0x0f, 0x2e, 0x6d,
|
|
||||||
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x17, 0x2e,
|
|
||||||
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x41, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64,
|
|
||||||
0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x4a, 0x0a, 0x11, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e,
|
|
||||||
0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x12, 0x1b, 0x2e, 0x6d, 0x65,
|
|
||||||
0x73, 0x73, 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, 0x73, 0x2e, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x41,
|
|
||||||
0x63, 0x6b, 0x12, 0x44, 0x0a, 0x0e, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x45, 0x78,
|
|
||||||
0x70, 0x69, 0x72, 0x79, 0x12, 0x18, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e,
|
|
||||||
0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x1a, 0x18,
|
|
||||||
0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x43,
|
|
||||||
0x68, 0x61, 0x6e, 0x67, 0x65, 0x41, 0x63, 0x6b, 0x12, 0x3c, 0x0a, 0x07, 0x43, 0x6c, 0x6f, 0x73,
|
|
||||||
0x69, 0x6e, 0x67, 0x12, 0x17, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x43,
|
|
||||||
0x6c, 0x6f, 0x73, 0x69, 0x6e, 0x67, 0x4e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x1a, 0x18, 0x2e, 0x6d,
|
|
||||||
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x43, 0x68, 0x61,
|
|
||||||
0x6e, 0x67, 0x65, 0x41, 0x63, 0x6b, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x2e, 0x74, 0x6f,
|
|
||||||
0x72, 0x6e, 0x62, 0x65, 0x72, 0x67, 0x2e, 0x6d, 0x65, 0x2f, 0x67, 0x6f, 0x2d, 0x63, 0x61, 0x72,
|
|
||||||
0x74, 0x2d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x3b, 0x6d, 0x65,
|
|
||||||
0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
|
||||||
})
|
|
||||||
|
|
||||||
var (
|
|
||||||
file_control_plane_proto_rawDescOnce sync.Once
|
|
||||||
file_control_plane_proto_rawDescData []byte
|
|
||||||
)
|
|
||||||
|
|
||||||
func file_control_plane_proto_rawDescGZIP() []byte {
|
|
||||||
file_control_plane_proto_rawDescOnce.Do(func() {
|
|
||||||
file_control_plane_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_control_plane_proto_rawDesc), len(file_control_plane_proto_rawDesc)))
|
|
||||||
})
|
|
||||||
return file_control_plane_proto_rawDescData
|
|
||||||
}
|
|
||||||
|
|
||||||
var file_control_plane_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
|
||||||
var file_control_plane_proto_goTypes = []any{
|
|
||||||
(*Empty)(nil), // 0: messages.Empty
|
|
||||||
(*PingReply)(nil), // 1: messages.PingReply
|
|
||||||
(*NegotiateRequest)(nil), // 2: messages.NegotiateRequest
|
|
||||||
(*NegotiateReply)(nil), // 3: messages.NegotiateReply
|
|
||||||
(*ActorIdsReply)(nil), // 4: messages.ActorIdsReply
|
|
||||||
(*OwnerChangeAck)(nil), // 5: messages.OwnerChangeAck
|
|
||||||
(*ClosingNotice)(nil), // 6: messages.ClosingNotice
|
|
||||||
(*OwnershipAnnounce)(nil), // 7: messages.OwnershipAnnounce
|
|
||||||
(*ExpiryAnnounce)(nil), // 8: messages.ExpiryAnnounce
|
|
||||||
}
|
|
||||||
var file_control_plane_proto_depIdxs = []int32{
|
|
||||||
0, // 0: messages.ControlPlane.Ping:input_type -> messages.Empty
|
|
||||||
2, // 1: messages.ControlPlane.Negotiate:input_type -> messages.NegotiateRequest
|
|
||||||
0, // 2: messages.ControlPlane.GetLocalActorIds:input_type -> messages.Empty
|
|
||||||
7, // 3: messages.ControlPlane.AnnounceOwnership:input_type -> messages.OwnershipAnnounce
|
|
||||||
8, // 4: messages.ControlPlane.AnnounceExpiry:input_type -> messages.ExpiryAnnounce
|
|
||||||
6, // 5: messages.ControlPlane.Closing:input_type -> messages.ClosingNotice
|
|
||||||
1, // 6: messages.ControlPlane.Ping:output_type -> messages.PingReply
|
|
||||||
3, // 7: messages.ControlPlane.Negotiate:output_type -> messages.NegotiateReply
|
|
||||||
4, // 8: messages.ControlPlane.GetLocalActorIds:output_type -> messages.ActorIdsReply
|
|
||||||
5, // 9: messages.ControlPlane.AnnounceOwnership:output_type -> messages.OwnerChangeAck
|
|
||||||
5, // 10: messages.ControlPlane.AnnounceExpiry:output_type -> messages.OwnerChangeAck
|
|
||||||
5, // 11: messages.ControlPlane.Closing:output_type -> messages.OwnerChangeAck
|
|
||||||
6, // [6:12] is the sub-list for method output_type
|
|
||||||
0, // [0:6] is the sub-list for method input_type
|
|
||||||
0, // [0:0] is the sub-list for extension type_name
|
|
||||||
0, // [0:0] is the sub-list for extension extendee
|
|
||||||
0, // [0:0] is the sub-list for field type_name
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() { file_control_plane_proto_init() }
|
|
||||||
func file_control_plane_proto_init() {
|
|
||||||
if File_control_plane_proto != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
type x struct{}
|
|
||||||
out := protoimpl.TypeBuilder{
|
|
||||||
File: protoimpl.DescBuilder{
|
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_control_plane_proto_rawDesc), len(file_control_plane_proto_rawDesc)),
|
|
||||||
NumEnums: 0,
|
|
||||||
NumMessages: 9,
|
|
||||||
NumExtensions: 0,
|
|
||||||
NumServices: 1,
|
|
||||||
},
|
|
||||||
GoTypes: file_control_plane_proto_goTypes,
|
|
||||||
DependencyIndexes: file_control_plane_proto_depIdxs,
|
|
||||||
MessageInfos: file_control_plane_proto_msgTypes,
|
|
||||||
}.Build()
|
|
||||||
File_control_plane_proto = out.File
|
|
||||||
file_control_plane_proto_goTypes = nil
|
|
||||||
file_control_plane_proto_depIdxs = nil
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.tornberg.me/go-cart-actor/pkg/cart"
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errInvalidTimeFormat = errors.New("invalid time format")
|
var errInvalidTimeFormat = errors.New("invalid time format")
|
||||||
@@ -26,7 +26,7 @@ func (NoopTracer) Trace(string, map[string]any) {}
|
|||||||
// for the purpose of promotion condition evaluation.
|
// for the purpose of promotion condition evaluation.
|
||||||
type PromotionItem struct {
|
type PromotionItem struct {
|
||||||
SKU string
|
SKU string
|
||||||
Quantity int
|
Quantity uint16
|
||||||
Category string
|
Category string
|
||||||
PriceIncVat int64
|
PriceIncVat int64
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ type PromotionItem struct {
|
|||||||
// customer/order metadata.
|
// customer/order metadata.
|
||||||
type PromotionEvalContext struct {
|
type PromotionEvalContext struct {
|
||||||
CartTotalIncVat int64
|
CartTotalIncVat int64
|
||||||
TotalItemQuantity int
|
TotalItemQuantity uint32
|
||||||
Items []PromotionItem
|
Items []PromotionItem
|
||||||
CustomerSegment string
|
CustomerSegment string
|
||||||
CustomerLifetimeValue float64
|
CustomerLifetimeValue float64
|
||||||
@@ -89,7 +89,7 @@ func NewContextFromCart(g *cart.CartGrain, opts ...ContextOption) *PromotionEval
|
|||||||
Category: strings.ToLower(category),
|
Category: strings.ToLower(category),
|
||||||
PriceIncVat: it.Price.IncVat,
|
PriceIncVat: it.Price.IncVat,
|
||||||
})
|
})
|
||||||
ctx.TotalItemQuantity += it.Quantity
|
ctx.TotalItemQuantity += uint32(it.Quantity)
|
||||||
}
|
}
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(ctx)
|
o(ctx)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.tornberg.me/go-cart-actor/pkg/cart"
|
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Helpers ---------------------------------------------------------------
|
// --- Helpers ---------------------------------------------------------------
|
||||||
@@ -58,7 +58,7 @@ func (t *testTracer) Count(name string) int {
|
|||||||
func makeCart(totalOverride int64, items []struct {
|
func makeCart(totalOverride int64, items []struct {
|
||||||
sku string
|
sku string
|
||||||
category string
|
category string
|
||||||
qty int
|
qty uint16
|
||||||
priceInc int64
|
priceInc int64
|
||||||
}) *cart.CartGrain {
|
}) *cart.CartGrain {
|
||||||
g := cart.NewCartGrain(1, time.Now())
|
g := cart.NewCartGrain(1, time.Now())
|
||||||
@@ -92,7 +92,7 @@ func TestEvaluateRuleBasicAND(t *testing.T) {
|
|||||||
g := makeCart(12000, []struct {
|
g := makeCart(12000, []struct {
|
||||||
sku string
|
sku string
|
||||||
category string
|
category string
|
||||||
qty int
|
qty uint16
|
||||||
priceInc int64
|
priceInc int64
|
||||||
}{
|
}{
|
||||||
{"SKU-1", "summer", 2, 3000},
|
{"SKU-1", "summer", 2, 3000},
|
||||||
@@ -226,7 +226,7 @@ func TestEvaluateProductCategoryIN(t *testing.T) {
|
|||||||
g := makeCart(-1, []struct {
|
g := makeCart(-1, []struct {
|
||||||
sku string
|
sku string
|
||||||
category string
|
category string
|
||||||
qty int
|
qty uint16
|
||||||
priceInc int64
|
priceInc int64
|
||||||
}{
|
}{
|
||||||
{"A", "shoes", 1, 5000},
|
{"A", "shoes", 1, 5000},
|
||||||
@@ -268,7 +268,7 @@ func TestEvaluateGroupOR(t *testing.T) {
|
|||||||
g := makeCart(3000, []struct {
|
g := makeCart(3000, []struct {
|
||||||
sku string
|
sku string
|
||||||
category string
|
category string
|
||||||
qty int
|
qty uint16
|
||||||
priceInc int64
|
priceInc int64
|
||||||
}{
|
}{
|
||||||
{"ONE", "x", 1, 3000},
|
{"ONE", "x", 1, 3000},
|
||||||
|
|||||||
+114
-15
@@ -1,7 +1,9 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -9,17 +11,20 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||||
|
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"
|
||||||
"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/types/known/anypb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RemoteHost mirrors the lightweight controller used for remote node
|
// RemoteHost mirrors the lightweight controller used for remote node
|
||||||
// interaction.
|
// interaction.
|
||||||
type RemoteHost struct {
|
type RemoteHost[V any] struct {
|
||||||
host string
|
host string
|
||||||
httpBase string
|
httpBase string
|
||||||
conn *grpc.ClientConn
|
conn *grpc.ClientConn
|
||||||
@@ -37,7 +42,34 @@ var (
|
|||||||
logger = otelslog.NewLogger(name)
|
logger = otelslog.NewLogger(name)
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewRemoteHost(host string) (*RemoteHost, error) {
|
// MockResponseWriter implements http.ResponseWriter to capture responses for proxy calls.
|
||||||
|
type MockResponseWriter struct {
|
||||||
|
StatusCode int
|
||||||
|
HeaderMap http.Header
|
||||||
|
Body *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMockResponseWriter() *MockResponseWriter {
|
||||||
|
return &MockResponseWriter{
|
||||||
|
StatusCode: 200,
|
||||||
|
HeaderMap: make(http.Header),
|
||||||
|
Body: &bytes.Buffer{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockResponseWriter) Header() http.Header {
|
||||||
|
return m.HeaderMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockResponseWriter) Write(data []byte) (int, error) {
|
||||||
|
return m.Body.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockResponseWriter) WriteHeader(statusCode int) {
|
||||||
|
m.StatusCode = statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRemoteHost[V any](host string) (*RemoteHost[V], error) {
|
||||||
|
|
||||||
target := fmt.Sprintf("%s:1337", host)
|
target := fmt.Sprintf("%s:1337", host)
|
||||||
|
|
||||||
@@ -58,7 +90,7 @@ func NewRemoteHost(host string) (*RemoteHost, error) {
|
|||||||
}
|
}
|
||||||
client := &http.Client{Transport: transport, Timeout: 10 * time.Second}
|
client := &http.Client{Transport: transport, Timeout: 10 * time.Second}
|
||||||
|
|
||||||
return &RemoteHost{
|
return &RemoteHost[V]{
|
||||||
host: host,
|
host: host,
|
||||||
httpBase: fmt.Sprintf("http://%s:8080", host),
|
httpBase: fmt.Sprintf("http://%s:8080", host),
|
||||||
conn: conn,
|
conn: conn,
|
||||||
@@ -69,18 +101,18 @@ func NewRemoteHost(host string) (*RemoteHost, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *RemoteHost) Name() string {
|
func (h *RemoteHost[V]) Name() string {
|
||||||
return h.host
|
return h.host
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *RemoteHost) Close() error {
|
func (h *RemoteHost[V]) Close() error {
|
||||||
if h.conn != nil {
|
if h.conn != nil {
|
||||||
h.conn.Close()
|
h.conn.Close()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *RemoteHost) Ping() bool {
|
func (h *RemoteHost[V]) Ping() bool {
|
||||||
var err error = errors.ErrUnsupported
|
var err error = errors.ErrUnsupported
|
||||||
for err != nil {
|
for err != nil {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
@@ -88,7 +120,6 @@ func (h *RemoteHost) Ping() bool {
|
|||||||
cancel()
|
cancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.missedPings++
|
h.missedPings++
|
||||||
log.Printf("Ping %s failed (%d) %v", h.host, h.missedPings, err)
|
|
||||||
}
|
}
|
||||||
if !h.IsHealthy() {
|
if !h.IsHealthy() {
|
||||||
return false
|
return false
|
||||||
@@ -100,7 +131,71 @@ func (h *RemoteHost) Ping() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *RemoteHost) Negotiate(knownHosts []string) ([]string, error) {
|
func (h *RemoteHost[V]) Get(ctx context.Context, id uint64) (*V, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
reply, error := h.controlClient.Get(ctx, &messages.GetRequest{Id: id})
|
||||||
|
if error != nil {
|
||||||
|
return nil, error
|
||||||
|
}
|
||||||
|
var grain V
|
||||||
|
err := json.Unmarshal(reply.Grain.Value, &grain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unpack state: %w", err)
|
||||||
|
}
|
||||||
|
return &grain, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RemoteHost[V]) Apply(ctx context.Context, id uint64, mutation ...proto.Message) (*actor.MutationResult[V], error) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
toSend := make([]*anypb.Any, len(mutation))
|
||||||
|
for i, msg := range mutation {
|
||||||
|
anyMsg, err := anypb.New(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to pack message: %w", err)
|
||||||
|
}
|
||||||
|
toSend[i] = anyMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.controlClient.Apply(ctx, &messages.ApplyRequest{
|
||||||
|
Id: id,
|
||||||
|
Messages: toSend,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
h.missedPings++
|
||||||
|
log.Printf("Apply %s failed: %v", h.host, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h.missedPings = 0
|
||||||
|
var grain V
|
||||||
|
err = json.Unmarshal(resp.State.Value, &grain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unpack state: %w", err)
|
||||||
|
}
|
||||||
|
var mutationList []actor.ApplyResult
|
||||||
|
for _, msg := range resp.Mutations {
|
||||||
|
|
||||||
|
mutation, err := anypb.UnmarshalNew(msg.Message, proto.UnmarshalOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unpack mutation: %w", err)
|
||||||
|
}
|
||||||
|
if msg.Error != nil {
|
||||||
|
err = errors.New(*msg.Error)
|
||||||
|
}
|
||||||
|
mutationList = append(mutationList, actor.ApplyResult{
|
||||||
|
Mutation: mutation,
|
||||||
|
Error: err,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
res := &actor.MutationResult[V]{
|
||||||
|
Result: grain,
|
||||||
|
Mutations: mutationList,
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RemoteHost[V]) Negotiate(knownHosts []string) ([]string, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -116,7 +211,7 @@ func (h *RemoteHost) Negotiate(knownHosts []string) ([]string, error) {
|
|||||||
return resp.Hosts, nil
|
return resp.Hosts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *RemoteHost) GetActorIds() []uint64 {
|
func (h *RemoteHost[V]) GetActorIds() []uint64 {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
reply, err := h.controlClient.GetLocalActorIds(ctx, &messages.Empty{})
|
reply, err := h.controlClient.GetLocalActorIds(ctx, &messages.Empty{})
|
||||||
@@ -128,7 +223,7 @@ func (h *RemoteHost) GetActorIds() []uint64 {
|
|||||||
return reply.GetIds()
|
return reply.GetIds()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *RemoteHost) AnnounceOwnership(ownerHost string, uids []uint64) {
|
func (h *RemoteHost[V]) AnnounceOwnership(ownerHost string, uids []uint64) {
|
||||||
_, err := h.controlClient.AnnounceOwnership(context.Background(), &messages.OwnershipAnnounce{
|
_, err := h.controlClient.AnnounceOwnership(context.Background(), &messages.OwnershipAnnounce{
|
||||||
Host: ownerHost,
|
Host: ownerHost,
|
||||||
Ids: uids,
|
Ids: uids,
|
||||||
@@ -141,7 +236,7 @@ func (h *RemoteHost) AnnounceOwnership(ownerHost string, uids []uint64) {
|
|||||||
h.missedPings = 0
|
h.missedPings = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *RemoteHost) AnnounceExpiry(uids []uint64) {
|
func (h *RemoteHost[V]) AnnounceExpiry(uids []uint64) {
|
||||||
_, err := h.controlClient.AnnounceExpiry(context.Background(), &messages.ExpiryAnnounce{
|
_, err := h.controlClient.AnnounceExpiry(context.Background(), &messages.ExpiryAnnounce{
|
||||||
Host: h.host,
|
Host: h.host,
|
||||||
Ids: uids,
|
Ids: uids,
|
||||||
@@ -154,7 +249,7 @@ func (h *RemoteHost) AnnounceExpiry(uids []uint64) {
|
|||||||
h.missedPings = 0
|
h.missedPings = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *RemoteHost) Proxy(id uint64, w http.ResponseWriter, r *http.Request) (bool, error) {
|
func (h *RemoteHost[V]) Proxy(id uint64, w http.ResponseWriter, r *http.Request, customBody io.Reader) (bool, error) {
|
||||||
target := fmt.Sprintf("%s%s", h.httpBase, r.URL.RequestURI())
|
target := fmt.Sprintf("%s%s", h.httpBase, r.URL.RequestURI())
|
||||||
|
|
||||||
ctx, span := tracer.Start(r.Context(), "remote_proxy")
|
ctx, span := tracer.Start(r.Context(), "remote_proxy")
|
||||||
@@ -168,7 +263,11 @@ func (h *RemoteHost) Proxy(id uint64, w http.ResponseWriter, r *http.Request) (b
|
|||||||
)
|
)
|
||||||
logger.InfoContext(ctx, "proxying request", "cartid", id, "host", h.host, "method", r.Method)
|
logger.InfoContext(ctx, "proxying request", "cartid", id, "host", h.host, "method", r.Method)
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, r.Method, target, r.Body)
|
var bdy io.Reader = r.Body
|
||||||
|
if customBody != nil {
|
||||||
|
bdy = customBody
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, r.Method, target, bdy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
span.RecordError(err)
|
span.RecordError(err)
|
||||||
http.Error(w, "proxy build error", http.StatusBadGateway)
|
http.Error(w, "proxy build error", http.StatusBadGateway)
|
||||||
@@ -207,6 +306,6 @@ func (h *RemoteHost) Proxy(id uint64, w http.ResponseWriter, r *http.Request) (b
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RemoteHost) IsHealthy() bool {
|
func (r *RemoteHost[V]) IsHealthy() bool {
|
||||||
return r.missedPings < 3
|
return r.missedPings < 3
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.tornberg.me/go-cart-actor/pkg/messages"
|
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Rule struct {
|
type Rule struct {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,113 @@
|
|||||||
|
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;
|
||||||
|
optional google.protobuf.Any sessionData = 8;
|
||||||
|
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 ContactDetailsUpdated {
|
||||||
|
optional string email = 1;
|
||||||
|
optional string phone = 2;
|
||||||
|
optional string name = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
ContactDetailsUpdated contact_details_updated = 6;
|
||||||
|
OrderCreated order_created = 7;
|
||||||
|
|
||||||
|
InitializeCheckout initialize_checkout = 9;
|
||||||
|
InventoryReserved inventory_reserved = 10;
|
||||||
|
PaymentStarted payment_started = 11;
|
||||||
|
PaymentCompleted payment_completed = 12;
|
||||||
|
PaymentEvent payment_event = 13;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,902 @@
|
|||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.36.5
|
||||||
|
// protoc v6.33.1
|
||||||
|
// source: control_plane.proto
|
||||||
|
|
||||||
|
package control_plane_messages
|
||||||
|
|
||||||
|
import (
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
anypb "google.golang.org/protobuf/types/known/anypb"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
|
unsafe "unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Empty request placeholder (common pattern).
|
||||||
|
type Empty struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Empty) Reset() {
|
||||||
|
*x = Empty{}
|
||||||
|
mi := &file_control_plane_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Empty) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Empty) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Empty) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_control_plane_proto_msgTypes[0]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use Empty.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Empty) Descriptor() ([]byte, []int) {
|
||||||
|
return file_control_plane_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping reply includes responding host and its current unix time (seconds).
|
||||||
|
type PingReply struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"`
|
||||||
|
UnixTime int64 `protobuf:"varint,2,opt,name=unix_time,json=unixTime,proto3" json:"unix_time,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PingReply) Reset() {
|
||||||
|
*x = PingReply{}
|
||||||
|
mi := &file_control_plane_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PingReply) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*PingReply) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *PingReply) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_control_plane_proto_msgTypes[1]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use PingReply.ProtoReflect.Descriptor instead.
|
||||||
|
func (*PingReply) Descriptor() ([]byte, []int) {
|
||||||
|
return file_control_plane_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PingReply) GetHost() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Host
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *PingReply) GetUnixTime() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.UnixTime
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// NegotiateRequest carries the caller's full view of known hosts (including self).
|
||||||
|
type NegotiateRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
KnownHosts []string `protobuf:"bytes,1,rep,name=known_hosts,json=knownHosts,proto3" json:"known_hosts,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *NegotiateRequest) Reset() {
|
||||||
|
*x = NegotiateRequest{}
|
||||||
|
mi := &file_control_plane_proto_msgTypes[2]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *NegotiateRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*NegotiateRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *NegotiateRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_control_plane_proto_msgTypes[2]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use NegotiateRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*NegotiateRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_control_plane_proto_rawDescGZIP(), []int{2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *NegotiateRequest) GetKnownHosts() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.KnownHosts
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NegotiateReply returns the callee's healthy hosts (including itself).
|
||||||
|
type NegotiateReply struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Hosts []string `protobuf:"bytes,1,rep,name=hosts,proto3" json:"hosts,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *NegotiateReply) Reset() {
|
||||||
|
*x = NegotiateReply{}
|
||||||
|
mi := &file_control_plane_proto_msgTypes[3]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *NegotiateReply) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*NegotiateReply) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *NegotiateReply) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_control_plane_proto_msgTypes[3]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use NegotiateReply.ProtoReflect.Descriptor instead.
|
||||||
|
func (*NegotiateReply) Descriptor() ([]byte, []int) {
|
||||||
|
return file_control_plane_proto_rawDescGZIP(), []int{3}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *NegotiateReply) GetHosts() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Hosts
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CartIdsReply returns the list of cart IDs (string form) currently owned locally.
|
||||||
|
type ActorIdsReply struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Ids []uint64 `protobuf:"varint,1,rep,packed,name=ids,proto3" json:"ids,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ActorIdsReply) Reset() {
|
||||||
|
*x = ActorIdsReply{}
|
||||||
|
mi := &file_control_plane_proto_msgTypes[4]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ActorIdsReply) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ActorIdsReply) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ActorIdsReply) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_control_plane_proto_msgTypes[4]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ActorIdsReply.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ActorIdsReply) Descriptor() ([]byte, []int) {
|
||||||
|
return file_control_plane_proto_rawDescGZIP(), []int{4}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ActorIdsReply) GetIds() []uint64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Ids
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OwnerChangeAck retained as response type for Closing RPC (ConfirmOwner removed).
|
||||||
|
type OwnerChangeAck struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Accepted bool `protobuf:"varint,1,opt,name=accepted,proto3" json:"accepted,omitempty"`
|
||||||
|
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *OwnerChangeAck) Reset() {
|
||||||
|
*x = OwnerChangeAck{}
|
||||||
|
mi := &file_control_plane_proto_msgTypes[5]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *OwnerChangeAck) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*OwnerChangeAck) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *OwnerChangeAck) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_control_plane_proto_msgTypes[5]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use OwnerChangeAck.ProtoReflect.Descriptor instead.
|
||||||
|
func (*OwnerChangeAck) Descriptor() ([]byte, []int) {
|
||||||
|
return file_control_plane_proto_rawDescGZIP(), []int{5}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *OwnerChangeAck) GetAccepted() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.Accepted
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *OwnerChangeAck) GetMessage() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Message
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClosingNotice notifies peers this host is terminating (so they can drop / re-resolve).
|
||||||
|
type ClosingNotice struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ClosingNotice) Reset() {
|
||||||
|
*x = ClosingNotice{}
|
||||||
|
mi := &file_control_plane_proto_msgTypes[6]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ClosingNotice) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ClosingNotice) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ClosingNotice) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_control_plane_proto_msgTypes[6]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ClosingNotice.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ClosingNotice) Descriptor() ([]byte, []int) {
|
||||||
|
return file_control_plane_proto_rawDescGZIP(), []int{6}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ClosingNotice) GetHost() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Host
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// OwnershipAnnounce broadcasts first-touch ownership claims for cart IDs.
|
||||||
|
// First claim wins; receivers SHOULD NOT overwrite an existing different owner.
|
||||||
|
type OwnershipAnnounce struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"` // announcing host
|
||||||
|
Ids []uint64 `protobuf:"varint,2,rep,packed,name=ids,proto3" json:"ids,omitempty"` // newly claimed cart ids
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *OwnershipAnnounce) Reset() {
|
||||||
|
*x = OwnershipAnnounce{}
|
||||||
|
mi := &file_control_plane_proto_msgTypes[7]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *OwnershipAnnounce) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*OwnershipAnnounce) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *OwnershipAnnounce) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_control_plane_proto_msgTypes[7]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use OwnershipAnnounce.ProtoReflect.Descriptor instead.
|
||||||
|
func (*OwnershipAnnounce) Descriptor() ([]byte, []int) {
|
||||||
|
return file_control_plane_proto_rawDescGZIP(), []int{7}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *OwnershipAnnounce) GetHost() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Host
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *OwnershipAnnounce) GetIds() []uint64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Ids
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiryAnnounce broadcasts that a host evicted the provided cart IDs.
|
||||||
|
type ExpiryAnnounce struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Host string `protobuf:"bytes,1,opt,name=host,proto3" json:"host,omitempty"`
|
||||||
|
Ids []uint64 `protobuf:"varint,2,rep,packed,name=ids,proto3" json:"ids,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExpiryAnnounce) Reset() {
|
||||||
|
*x = ExpiryAnnounce{}
|
||||||
|
mi := &file_control_plane_proto_msgTypes[8]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExpiryAnnounce) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ExpiryAnnounce) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ExpiryAnnounce) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_control_plane_proto_msgTypes[8]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ExpiryAnnounce.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ExpiryAnnounce) Descriptor() ([]byte, []int) {
|
||||||
|
return file_control_plane_proto_rawDescGZIP(), []int{8}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExpiryAnnounce) GetHost() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Host
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ExpiryAnnounce) GetIds() []uint64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Ids
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApplyRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
|
Messages []*anypb.Any `protobuf:"bytes,2,rep,name=messages,proto3" json:"messages,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ApplyRequest) Reset() {
|
||||||
|
*x = ApplyRequest{}
|
||||||
|
mi := &file_control_plane_proto_msgTypes[9]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ApplyRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ApplyRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ApplyRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_control_plane_proto_msgTypes[9]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ApplyRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ApplyRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_control_plane_proto_rawDescGZIP(), []int{9}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ApplyRequest) GetId() uint64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ApplyRequest) GetMessages() []*anypb.Any {
|
||||||
|
if x != nil {
|
||||||
|
return x.Messages
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetRequest struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetRequest) Reset() {
|
||||||
|
*x = GetRequest{}
|
||||||
|
mi := &file_control_plane_proto_msgTypes[10]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*GetRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *GetRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_control_plane_proto_msgTypes[10]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use GetRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*GetRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_control_plane_proto_rawDescGZIP(), []int{10}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetRequest) GetId() uint64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetReply struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Grain *anypb.Any `protobuf:"bytes,1,opt,name=grain,proto3" json:"grain,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetReply) Reset() {
|
||||||
|
*x = GetReply{}
|
||||||
|
mi := &file_control_plane_proto_msgTypes[11]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetReply) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*GetReply) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *GetReply) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_control_plane_proto_msgTypes[11]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use GetReply.ProtoReflect.Descriptor instead.
|
||||||
|
func (*GetReply) Descriptor() ([]byte, []int) {
|
||||||
|
return file_control_plane_proto_rawDescGZIP(), []int{11}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *GetReply) GetGrain() *anypb.Any {
|
||||||
|
if x != nil {
|
||||||
|
return x.Grain
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MutationResult struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
|
||||||
|
Message *anypb.Any `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
|
||||||
|
Error *string `protobuf:"bytes,3,opt,name=error,proto3,oneof" json:"error,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MutationResult) Reset() {
|
||||||
|
*x = MutationResult{}
|
||||||
|
mi := &file_control_plane_proto_msgTypes[12]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MutationResult) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MutationResult) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *MutationResult) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_control_plane_proto_msgTypes[12]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use MutationResult.ProtoReflect.Descriptor instead.
|
||||||
|
func (*MutationResult) Descriptor() ([]byte, []int) {
|
||||||
|
return file_control_plane_proto_rawDescGZIP(), []int{12}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MutationResult) GetType() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Type
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MutationResult) GetMessage() *anypb.Any {
|
||||||
|
if x != nil {
|
||||||
|
return x.Message
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *MutationResult) GetError() string {
|
||||||
|
if x != nil && x.Error != nil {
|
||||||
|
return *x.Error
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApplyResult struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
State *anypb.Any `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"`
|
||||||
|
Mutations []*MutationResult `protobuf:"bytes,2,rep,name=mutations,proto3" json:"mutations,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ApplyResult) Reset() {
|
||||||
|
*x = ApplyResult{}
|
||||||
|
mi := &file_control_plane_proto_msgTypes[13]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ApplyResult) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ApplyResult) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ApplyResult) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_control_plane_proto_msgTypes[13]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ApplyResult.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ApplyResult) Descriptor() ([]byte, []int) {
|
||||||
|
return file_control_plane_proto_rawDescGZIP(), []int{13}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ApplyResult) GetState() *anypb.Any {
|
||||||
|
if x != nil {
|
||||||
|
return x.State
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ApplyResult) GetMutations() []*MutationResult {
|
||||||
|
if x != nil {
|
||||||
|
return x.Mutations
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_control_plane_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
var file_control_plane_proto_rawDesc = string([]byte{
|
||||||
|
0x0a, 0x13, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e,
|
||||||
|
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x16, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70,
|
||||||
|
0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x1a, 0x19, 0x67,
|
||||||
|
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61,
|
||||||
|
0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74,
|
||||||
|
0x79, 0x22, 0x3c, 0x0a, 0x09, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x12,
|
||||||
|
0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f,
|
||||||
|
0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x75, 0x6e, 0x69, 0x78, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18,
|
||||||
|
0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x75, 0x6e, 0x69, 0x78, 0x54, 0x69, 0x6d, 0x65, 0x22,
|
||||||
|
0x33, 0x0a, 0x10, 0x4e, 0x65, 0x67, 0x6f, 0x74, 0x69, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75,
|
||||||
|
0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x5f, 0x68, 0x6f, 0x73,
|
||||||
|
0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x48,
|
||||||
|
0x6f, 0x73, 0x74, 0x73, 0x22, 0x26, 0x0a, 0x0e, 0x4e, 0x65, 0x67, 0x6f, 0x74, 0x69, 0x61, 0x74,
|
||||||
|
0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x18,
|
||||||
|
0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x22, 0x21, 0x0a, 0x0d,
|
||||||
|
0x41, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x10, 0x0a,
|
||||||
|
0x03, 0x69, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x04, 0x52, 0x03, 0x69, 0x64, 0x73, 0x22,
|
||||||
|
0x46, 0x0a, 0x0e, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x41, 0x63,
|
||||||
|
0x6b, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20,
|
||||||
|
0x01, 0x28, 0x08, 0x52, 0x08, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a,
|
||||||
|
0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
|
||||||
|
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x23, 0x0a, 0x0d, 0x43, 0x6c, 0x6f, 0x73, 0x69,
|
||||||
|
0x6e, 0x67, 0x4e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74,
|
||||||
|
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x22, 0x39, 0x0a, 0x11,
|
||||||
|
0x4f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63,
|
||||||
|
0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
|
0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03,
|
||||||
|
0x28, 0x04, 0x52, 0x03, 0x69, 0x64, 0x73, 0x22, 0x36, 0x0a, 0x0e, 0x45, 0x78, 0x70, 0x69, 0x72,
|
||||||
|
0x79, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73,
|
||||||
|
0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x10, 0x0a,
|
||||||
|
0x03, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x04, 0x52, 0x03, 0x69, 0x64, 0x73, 0x22,
|
||||||
|
0x50, 0x0a, 0x0c, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
||||||
|
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12,
|
||||||
|
0x30, 0x0a, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
|
||||||
|
0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||||
|
0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
|
||||||
|
0x73, 0x22, 0x1c, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
|
||||||
|
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x22,
|
||||||
|
0x36, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x67,
|
||||||
|
0x72, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f,
|
||||||
|
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79,
|
||||||
|
0x52, 0x05, 0x67, 0x72, 0x61, 0x69, 0x6e, 0x22, 0x79, 0x0a, 0x0e, 0x4d, 0x75, 0x74, 0x61, 0x74,
|
||||||
|
0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70,
|
||||||
|
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2e, 0x0a,
|
||||||
|
0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14,
|
||||||
|
0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
|
||||||
|
0x2e, 0x41, 0x6e, 0x79, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x19, 0x0a,
|
||||||
|
0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x05,
|
||||||
|
0x65, 0x72, 0x72, 0x6f, 0x72, 0x88, 0x01, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x65, 0x72, 0x72,
|
||||||
|
0x6f, 0x72, 0x22, 0x7f, 0x0a, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x73, 0x75, 0x6c,
|
||||||
|
0x74, 0x12, 0x2a, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
|
||||||
|
0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
|
||||||
|
0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x44, 0x0a,
|
||||||
|
0x09, 0x6d, 0x75, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b,
|
||||||
|
0x32, 0x26, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65,
|
||||||
|
0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x4d, 0x75, 0x74, 0x61, 0x74, 0x69,
|
||||||
|
0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x09, 0x6d, 0x75, 0x74, 0x61, 0x74, 0x69,
|
||||||
|
0x6f, 0x6e, 0x73, 0x32, 0xd6, 0x05, 0x0a, 0x0c, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x50,
|
||||||
|
0x6c, 0x61, 0x6e, 0x65, 0x12, 0x48, 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 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, 0x21, 0x2e, 0x63, 0x6f,
|
||||||
|
0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73,
|
||||||
|
0x61, 0x67, 0x65, 0x73, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x5d,
|
||||||
|
0x0a, 0x09, 0x4e, 0x65, 0x67, 0x6f, 0x74, 0x69, 0x61, 0x74, 0x65, 0x12, 0x28, 0x2e, 0x63, 0x6f,
|
||||||
|
0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73,
|
||||||
|
0x61, 0x67, 0x65, 0x73, 0x2e, 0x4e, 0x65, 0x67, 0x6f, 0x74, 0x69, 0x61, 0x74, 0x65, 0x52, 0x65,
|
||||||
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f,
|
||||||
|
0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x4e,
|
||||||
|
0x65, 0x67, 0x6f, 0x74, 0x69, 0x61, 0x74, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x58, 0x0a,
|
||||||
|
0x10, 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, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x1a, 0x26, 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, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x41, 0x63, 0x6b, 0x12,
|
||||||
|
0x52, 0x0a, 0x05, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x12, 0x24, 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, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23,
|
||||||
|
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, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x73,
|
||||||
|
0x75, 0x6c, 0x74, 0x12, 0x60, 0x0a, 0x0e, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x45,
|
||||||
|
0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x26, 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,
|
||||||
|
0x78, 0x70, 0x69, 0x72, 0x79, 0x41, 0x6e, 0x6e, 0x6f, 0x75, 0x6e, 0x63, 0x65, 0x1a, 0x26, 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, 0x43, 0x68, 0x61, 0x6e,
|
||||||
|
0x67, 0x65, 0x41, 0x63, 0x6b, 0x12, 0x58, 0x0a, 0x07, 0x43, 0x6c, 0x6f, 0x73, 0x69, 0x6e, 0x67,
|
||||||
|
0x12, 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, 0x43, 0x6c, 0x6f, 0x73, 0x69, 0x6e,
|
||||||
|
0x67, 0x4e, 0x6f, 0x74, 0x69, 0x63, 0x65, 0x1a, 0x26, 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, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x41, 0x63, 0x6b, 0x12,
|
||||||
|
0x4b, 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 (
|
||||||
|
file_control_plane_proto_rawDescOnce sync.Once
|
||||||
|
file_control_plane_proto_rawDescData []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_control_plane_proto_rawDescGZIP() []byte {
|
||||||
|
file_control_plane_proto_rawDescOnce.Do(func() {
|
||||||
|
file_control_plane_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_control_plane_proto_rawDesc), len(file_control_plane_proto_rawDesc)))
|
||||||
|
})
|
||||||
|
return file_control_plane_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_control_plane_proto_msgTypes = make([]protoimpl.MessageInfo, 14)
|
||||||
|
var file_control_plane_proto_goTypes = []any{
|
||||||
|
(*Empty)(nil), // 0: control_plane_messages.Empty
|
||||||
|
(*PingReply)(nil), // 1: control_plane_messages.PingReply
|
||||||
|
(*NegotiateRequest)(nil), // 2: control_plane_messages.NegotiateRequest
|
||||||
|
(*NegotiateReply)(nil), // 3: control_plane_messages.NegotiateReply
|
||||||
|
(*ActorIdsReply)(nil), // 4: control_plane_messages.ActorIdsReply
|
||||||
|
(*OwnerChangeAck)(nil), // 5: control_plane_messages.OwnerChangeAck
|
||||||
|
(*ClosingNotice)(nil), // 6: control_plane_messages.ClosingNotice
|
||||||
|
(*OwnershipAnnounce)(nil), // 7: control_plane_messages.OwnershipAnnounce
|
||||||
|
(*ExpiryAnnounce)(nil), // 8: control_plane_messages.ExpiryAnnounce
|
||||||
|
(*ApplyRequest)(nil), // 9: control_plane_messages.ApplyRequest
|
||||||
|
(*GetRequest)(nil), // 10: control_plane_messages.GetRequest
|
||||||
|
(*GetReply)(nil), // 11: control_plane_messages.GetReply
|
||||||
|
(*MutationResult)(nil), // 12: control_plane_messages.MutationResult
|
||||||
|
(*ApplyResult)(nil), // 13: control_plane_messages.ApplyResult
|
||||||
|
(*anypb.Any)(nil), // 14: google.protobuf.Any
|
||||||
|
}
|
||||||
|
var file_control_plane_proto_depIdxs = []int32{
|
||||||
|
14, // 0: control_plane_messages.ApplyRequest.messages:type_name -> google.protobuf.Any
|
||||||
|
14, // 1: control_plane_messages.GetReply.grain:type_name -> google.protobuf.Any
|
||||||
|
14, // 2: control_plane_messages.MutationResult.message:type_name -> google.protobuf.Any
|
||||||
|
14, // 3: control_plane_messages.ApplyResult.state:type_name -> google.protobuf.Any
|
||||||
|
12, // 4: control_plane_messages.ApplyResult.mutations:type_name -> control_plane_messages.MutationResult
|
||||||
|
0, // 5: control_plane_messages.ControlPlane.Ping:input_type -> control_plane_messages.Empty
|
||||||
|
2, // 6: control_plane_messages.ControlPlane.Negotiate:input_type -> control_plane_messages.NegotiateRequest
|
||||||
|
0, // 7: control_plane_messages.ControlPlane.GetLocalActorIds:input_type -> control_plane_messages.Empty
|
||||||
|
7, // 8: control_plane_messages.ControlPlane.AnnounceOwnership:input_type -> control_plane_messages.OwnershipAnnounce
|
||||||
|
9, // 9: control_plane_messages.ControlPlane.Apply:input_type -> control_plane_messages.ApplyRequest
|
||||||
|
8, // 10: control_plane_messages.ControlPlane.AnnounceExpiry:input_type -> control_plane_messages.ExpiryAnnounce
|
||||||
|
6, // 11: control_plane_messages.ControlPlane.Closing:input_type -> control_plane_messages.ClosingNotice
|
||||||
|
10, // 12: control_plane_messages.ControlPlane.Get:input_type -> control_plane_messages.GetRequest
|
||||||
|
1, // 13: control_plane_messages.ControlPlane.Ping:output_type -> control_plane_messages.PingReply
|
||||||
|
3, // 14: control_plane_messages.ControlPlane.Negotiate:output_type -> control_plane_messages.NegotiateReply
|
||||||
|
4, // 15: control_plane_messages.ControlPlane.GetLocalActorIds:output_type -> control_plane_messages.ActorIdsReply
|
||||||
|
5, // 16: control_plane_messages.ControlPlane.AnnounceOwnership:output_type -> control_plane_messages.OwnerChangeAck
|
||||||
|
13, // 17: control_plane_messages.ControlPlane.Apply:output_type -> control_plane_messages.ApplyResult
|
||||||
|
5, // 18: control_plane_messages.ControlPlane.AnnounceExpiry:output_type -> control_plane_messages.OwnerChangeAck
|
||||||
|
5, // 19: control_plane_messages.ControlPlane.Closing:output_type -> control_plane_messages.OwnerChangeAck
|
||||||
|
11, // 20: control_plane_messages.ControlPlane.Get:output_type -> control_plane_messages.GetReply
|
||||||
|
13, // [13:21] is the sub-list for method output_type
|
||||||
|
5, // [5:13] is the sub-list for method input_type
|
||||||
|
5, // [5:5] is the sub-list for extension type_name
|
||||||
|
5, // [5:5] is the sub-list for extension extendee
|
||||||
|
0, // [0:5] is the sub-list for field type_name
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_control_plane_proto_init() }
|
||||||
|
func file_control_plane_proto_init() {
|
||||||
|
if File_control_plane_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file_control_plane_proto_msgTypes[12].OneofWrappers = []any{}
|
||||||
|
type x struct{}
|
||||||
|
out := protoimpl.TypeBuilder{
|
||||||
|
File: protoimpl.DescBuilder{
|
||||||
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_control_plane_proto_rawDesc), len(file_control_plane_proto_rawDesc)),
|
||||||
|
NumEnums: 0,
|
||||||
|
NumMessages: 14,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 1,
|
||||||
|
},
|
||||||
|
GoTypes: file_control_plane_proto_goTypes,
|
||||||
|
DependencyIndexes: file_control_plane_proto_depIdxs,
|
||||||
|
MessageInfos: file_control_plane_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_control_plane_proto = out.File
|
||||||
|
file_control_plane_proto_goTypes = nil
|
||||||
|
file_control_plane_proto_depIdxs = nil
|
||||||
|
}
|
||||||
@@ -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,12 +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_AnnounceExpiry_FullMethodName = "/messages.ControlPlane/AnnounceExpiry"
|
ControlPlane_Apply_FullMethodName = "/control_plane_messages.ControlPlane/Apply"
|
||||||
ControlPlane_Closing_FullMethodName = "/messages.ControlPlane/Closing"
|
ControlPlane_AnnounceExpiry_FullMethodName = "/control_plane_messages.ControlPlane/AnnounceExpiry"
|
||||||
|
ControlPlane_Closing_FullMethodName = "/control_plane_messages.ControlPlane/Closing"
|
||||||
|
ControlPlane_Get_FullMethodName = "/control_plane_messages.ControlPlane/Get"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ControlPlaneClient is the client API for ControlPlane service.
|
// ControlPlaneClient is the client API for ControlPlane service.
|
||||||
@@ -41,10 +43,12 @@ type ControlPlaneClient interface {
|
|||||||
GetLocalActorIds(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*ActorIdsReply, error)
|
GetLocalActorIds(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*ActorIdsReply, error)
|
||||||
// Ownership announcement: first-touch claim broadcast (idempotent; best-effort).
|
// Ownership announcement: first-touch claim broadcast (idempotent; best-effort).
|
||||||
AnnounceOwnership(ctx context.Context, in *OwnershipAnnounce, opts ...grpc.CallOption) (*OwnerChangeAck, error)
|
AnnounceOwnership(ctx context.Context, in *OwnershipAnnounce, opts ...grpc.CallOption) (*OwnerChangeAck, error)
|
||||||
|
Apply(ctx context.Context, in *ApplyRequest, opts ...grpc.CallOption) (*ApplyResult, error)
|
||||||
// Expiry announcement: drop remote ownership hints when local TTL expires.
|
// Expiry announcement: drop remote ownership hints when local TTL expires.
|
||||||
AnnounceExpiry(ctx context.Context, in *ExpiryAnnounce, opts ...grpc.CallOption) (*OwnerChangeAck, error)
|
AnnounceExpiry(ctx context.Context, in *ExpiryAnnounce, opts ...grpc.CallOption) (*OwnerChangeAck, error)
|
||||||
// Closing announces graceful shutdown so peers can proactively adjust.
|
// Closing announces graceful shutdown so peers can proactively adjust.
|
||||||
Closing(ctx context.Context, in *ClosingNotice, opts ...grpc.CallOption) (*OwnerChangeAck, error)
|
Closing(ctx context.Context, in *ClosingNotice, opts ...grpc.CallOption) (*OwnerChangeAck, error)
|
||||||
|
Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetReply, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type controlPlaneClient struct {
|
type controlPlaneClient struct {
|
||||||
@@ -95,6 +99,16 @@ func (c *controlPlaneClient) AnnounceOwnership(ctx context.Context, in *Ownershi
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *controlPlaneClient) Apply(ctx context.Context, in *ApplyRequest, opts ...grpc.CallOption) (*ApplyResult, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ApplyResult)
|
||||||
|
err := c.cc.Invoke(ctx, ControlPlane_Apply_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *controlPlaneClient) AnnounceExpiry(ctx context.Context, in *ExpiryAnnounce, opts ...grpc.CallOption) (*OwnerChangeAck, error) {
|
func (c *controlPlaneClient) AnnounceExpiry(ctx context.Context, in *ExpiryAnnounce, opts ...grpc.CallOption) (*OwnerChangeAck, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(OwnerChangeAck)
|
out := new(OwnerChangeAck)
|
||||||
@@ -115,6 +129,16 @@ func (c *controlPlaneClient) Closing(ctx context.Context, in *ClosingNotice, opt
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *controlPlaneClient) Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetReply, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(GetReply)
|
||||||
|
err := c.cc.Invoke(ctx, ControlPlane_Get_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ControlPlaneServer is the server API for ControlPlane service.
|
// ControlPlaneServer is the server API for ControlPlane service.
|
||||||
// All implementations must embed UnimplementedControlPlaneServer
|
// All implementations must embed UnimplementedControlPlaneServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
@@ -129,10 +153,12 @@ type ControlPlaneServer interface {
|
|||||||
GetLocalActorIds(context.Context, *Empty) (*ActorIdsReply, error)
|
GetLocalActorIds(context.Context, *Empty) (*ActorIdsReply, error)
|
||||||
// Ownership announcement: first-touch claim broadcast (idempotent; best-effort).
|
// Ownership announcement: first-touch claim broadcast (idempotent; best-effort).
|
||||||
AnnounceOwnership(context.Context, *OwnershipAnnounce) (*OwnerChangeAck, error)
|
AnnounceOwnership(context.Context, *OwnershipAnnounce) (*OwnerChangeAck, error)
|
||||||
|
Apply(context.Context, *ApplyRequest) (*ApplyResult, error)
|
||||||
// Expiry announcement: drop remote ownership hints when local TTL expires.
|
// Expiry announcement: drop remote ownership hints when local TTL expires.
|
||||||
AnnounceExpiry(context.Context, *ExpiryAnnounce) (*OwnerChangeAck, error)
|
AnnounceExpiry(context.Context, *ExpiryAnnounce) (*OwnerChangeAck, error)
|
||||||
// Closing announces graceful shutdown so peers can proactively adjust.
|
// Closing announces graceful shutdown so peers can proactively adjust.
|
||||||
Closing(context.Context, *ClosingNotice) (*OwnerChangeAck, error)
|
Closing(context.Context, *ClosingNotice) (*OwnerChangeAck, error)
|
||||||
|
Get(context.Context, *GetRequest) (*GetReply, error)
|
||||||
mustEmbedUnimplementedControlPlaneServer()
|
mustEmbedUnimplementedControlPlaneServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,12 +181,18 @@ func (UnimplementedControlPlaneServer) GetLocalActorIds(context.Context, *Empty)
|
|||||||
func (UnimplementedControlPlaneServer) AnnounceOwnership(context.Context, *OwnershipAnnounce) (*OwnerChangeAck, error) {
|
func (UnimplementedControlPlaneServer) AnnounceOwnership(context.Context, *OwnershipAnnounce) (*OwnerChangeAck, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method AnnounceOwnership not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method AnnounceOwnership not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedControlPlaneServer) Apply(context.Context, *ApplyRequest) (*ApplyResult, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method Apply not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedControlPlaneServer) AnnounceExpiry(context.Context, *ExpiryAnnounce) (*OwnerChangeAck, error) {
|
func (UnimplementedControlPlaneServer) AnnounceExpiry(context.Context, *ExpiryAnnounce) (*OwnerChangeAck, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method AnnounceExpiry not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method AnnounceExpiry not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedControlPlaneServer) Closing(context.Context, *ClosingNotice) (*OwnerChangeAck, error) {
|
func (UnimplementedControlPlaneServer) Closing(context.Context, *ClosingNotice) (*OwnerChangeAck, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method Closing not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method Closing not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedControlPlaneServer) Get(context.Context, *GetRequest) (*GetReply, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method Get not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedControlPlaneServer) mustEmbedUnimplementedControlPlaneServer() {}
|
func (UnimplementedControlPlaneServer) mustEmbedUnimplementedControlPlaneServer() {}
|
||||||
func (UnimplementedControlPlaneServer) testEmbeddedByValue() {}
|
func (UnimplementedControlPlaneServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@@ -254,6 +286,24 @@ func _ControlPlane_AnnounceOwnership_Handler(srv interface{}, ctx context.Contex
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _ControlPlane_Apply_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ApplyRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ControlPlaneServer).Apply(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: ControlPlane_Apply_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ControlPlaneServer).Apply(ctx, req.(*ApplyRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _ControlPlane_AnnounceExpiry_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _ControlPlane_AnnounceExpiry_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(ExpiryAnnounce)
|
in := new(ExpiryAnnounce)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@@ -290,11 +340,29 @@ func _ControlPlane_Closing_Handler(srv interface{}, ctx context.Context, dec fun
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _ControlPlane_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ControlPlaneServer).Get(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: ControlPlane_Get_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ControlPlaneServer).Get(ctx, req.(*GetRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
// ControlPlane_ServiceDesc is the grpc.ServiceDesc for ControlPlane service.
|
// ControlPlane_ServiceDesc is the grpc.ServiceDesc for ControlPlane service.
|
||||||
// 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{
|
||||||
{
|
{
|
||||||
@@ -313,6 +381,10 @@ var ControlPlane_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "AnnounceOwnership",
|
MethodName: "AnnounceOwnership",
|
||||||
Handler: _ControlPlane_AnnounceOwnership_Handler,
|
Handler: _ControlPlane_AnnounceOwnership_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Apply",
|
||||||
|
Handler: _ControlPlane_Apply_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "AnnounceExpiry",
|
MethodName: "AnnounceExpiry",
|
||||||
Handler: _ControlPlane_AnnounceExpiry_Handler,
|
Handler: _ControlPlane_AnnounceExpiry_Handler,
|
||||||
@@ -321,6 +393,10 @@ var ControlPlane_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "Closing",
|
MethodName: "Closing",
|
||||||
Handler: _ControlPlane_Closing_Handler,
|
Handler: _ControlPlane_Closing_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Get",
|
||||||
|
Handler: _ControlPlane_Get_Handler,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Streams: []grpc.StreamDesc{},
|
Streams: []grpc.StreamDesc{},
|
||||||
Metadata: "control_plane.proto",
|
Metadata: "control_plane.proto",
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
|
|
||||||
package messages;
|
package control_plane_messages;
|
||||||
|
|
||||||
option go_package = "git.tornberg.me/go-cart-actor/proto;messages";
|
option go_package = "git.k6n.net/go-cart-actor/proto/control;control_plane_messages";
|
||||||
|
|
||||||
|
import "google/protobuf/any.proto";
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Control Plane gRPC API
|
// Control Plane gRPC API
|
||||||
@@ -65,6 +67,31 @@ message ExpiryAnnounce {
|
|||||||
repeated uint64 ids = 2;
|
repeated uint64 ids = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ApplyRequest {
|
||||||
|
|
||||||
|
uint64 id = 1;
|
||||||
|
repeated google.protobuf.Any messages = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetRequest {
|
||||||
|
uint64 id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetReply {
|
||||||
|
google.protobuf.Any grain = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MutationResult {
|
||||||
|
string type = 1;
|
||||||
|
google.protobuf.Any message = 2;
|
||||||
|
optional string error = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ApplyResult {
|
||||||
|
google.protobuf.Any state = 1;
|
||||||
|
repeated MutationResult mutations = 2;
|
||||||
|
}
|
||||||
|
|
||||||
// ControlPlane defines cluster coordination and ownership operations.
|
// ControlPlane defines cluster coordination and ownership operations.
|
||||||
service ControlPlane {
|
service ControlPlane {
|
||||||
// Ping for liveness; lightweight health signal.
|
// Ping for liveness; lightweight health signal.
|
||||||
@@ -80,12 +107,14 @@ service ControlPlane {
|
|||||||
|
|
||||||
// Ownership announcement: first-touch claim broadcast (idempotent; best-effort).
|
// Ownership announcement: first-touch claim broadcast (idempotent; best-effort).
|
||||||
rpc AnnounceOwnership(OwnershipAnnounce) returns (OwnerChangeAck);
|
rpc AnnounceOwnership(OwnershipAnnounce) returns (OwnerChangeAck);
|
||||||
|
rpc Apply(ApplyRequest) returns (ApplyResult);
|
||||||
|
|
||||||
// Expiry announcement: drop remote ownership hints when local TTL expires.
|
// Expiry announcement: drop remote ownership hints when local TTL expires.
|
||||||
rpc AnnounceExpiry(ExpiryAnnounce) returns (OwnerChangeAck);
|
rpc AnnounceExpiry(ExpiryAnnounce) returns (OwnerChangeAck);
|
||||||
|
|
||||||
// Closing announces graceful shutdown so peers can proactively adjust.
|
// Closing announces graceful shutdown so peers can proactively adjust.
|
||||||
rpc Closing(ClosingNotice) returns (OwnerChangeAck);
|
rpc Closing(ClosingNotice) returns (OwnerChangeAck);
|
||||||
|
rpc Get(GetRequest) returns (GetReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
package messages;
|
|
||||||
option go_package = "git.tornberg.me/go-cart-actor/proto;messages";
|
|
||||||
|
|
||||||
import "google/protobuf/any.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 PaymentDeclined {
|
|
||||||
string message = 1;
|
|
||||||
optional string code = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ConfirmationViewed {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user