Compare commits
129
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 | ||
|
|
896bf7672b | ||
|
|
c859796b1e | ||
|
|
a01feb615b | ||
|
|
f475b56659 | ||
|
|
430da09264 | ||
|
|
729c67f992 | ||
|
|
60cd6cfd51 | ||
|
|
1c8e9cc974 | ||
|
|
874963812f | ||
|
|
67f63244bb | ||
|
|
270539a2fe | ||
|
|
dd5de0b1a1 | ||
|
|
3e78dd8b00 | ||
|
|
13dcb1ec45 | ||
|
|
4fa78e786f | ||
|
|
c917bc62fb | ||
|
|
ac8b870eb5 | ||
|
|
cdece4e0d2 | ||
|
|
c9d9d62bfb | ||
|
|
e662192b56 | ||
|
|
a7aab5161b |
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"description": "Project Overview",
|
||||
"rule": "This is a distributed cart management system in Go using the actor model. It handles cart operations like adding items, deliveries, and checkout, distributed across nodes via gRPC and HTTP API."
|
||||
},
|
||||
{
|
||||
"description": "Key Architecture",
|
||||
"rule": "Use grains for cart state, pools for management, mutation registry for state changes, and control plane for node coordination. Ownership via consistent hashing ring."
|
||||
},
|
||||
{
|
||||
"description": "Coding Standards",
|
||||
"rule": "Follow Go conventions. Use mutation registry for all state changes. Regenerate protobuf code after proto changes; never edit .pb.go files. Handle errors properly, use cookies for cart IDs in HTTP."
|
||||
},
|
||||
{
|
||||
"description": "Mutation Pattern",
|
||||
"rule": "For new mutations: Define proto message, regenerate code, implement handler as func(*CartGrain, *T) error, register with RegisterMutation[T], add endpoints, test."
|
||||
},
|
||||
{
|
||||
"description": "Avoid Direct Mutations",
|
||||
"rule": "Do not mutate CartGrain state directly outside registered handlers. Use the registry for consistency."
|
||||
},
|
||||
{
|
||||
"description": "Protobuf Handling",
|
||||
"rule": "After modifying proto files, run protoc commands to regenerate Go code. Ensure protoc-gen-go and protoc-gen-go-grpc are installed."
|
||||
},
|
||||
{
|
||||
"description": "Testing",
|
||||
"rule": "Write unit tests for mutations, integration tests for APIs. Run go test ./... regularly."
|
||||
},
|
||||
{
|
||||
"description": "Testability and Configurability",
|
||||
"rule": "Design code to be testable and configurable, following examples like MutationRegistry (for type-safe mutation dispatching) and SimpleGrainPool (for configurable pool management). Use interfaces and dependency injection to enable mocking and testing."
|
||||
},
|
||||
{
|
||||
"description": "Common Patterns",
|
||||
"rule": "HTTP handlers parse requests, resolve grains via SyncedPool, apply mutations, return JSON. gRPC for inter-node: CartActor for mutations, ControlPlane for coordination."
|
||||
}
|
||||
]
|
||||
}
|
||||
+15
-10
@@ -1,6 +1,9 @@
|
||||
name: Build and Publish
|
||||
run-name: ${{ gitea.actor }} build 🚀
|
||||
on: [push]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
BuildAndDeployAmd64:
|
||||
@@ -11,32 +14,34 @@ jobs:
|
||||
run: |
|
||||
docker build \
|
||||
--progress=plain \
|
||||
-t registry.knatofs.se/go-cart-actor-amd64:latest \
|
||||
-t registry.k6n.net/go-cart-actor-amd64:latest \
|
||||
.
|
||||
- name: Push amd64 images
|
||||
run: |
|
||||
docker push registry.knatofs.se/go-cart-actor-amd64:latest
|
||||
docker push registry.k6n.net/go-cart-actor-amd64:latest
|
||||
- name: Apply deployment manifests
|
||||
run: kubectl apply -f deployment/deployment.yaml -n cart
|
||||
- name: Rollout amd64 backoffice deployment
|
||||
run: |
|
||||
kubectl rollout restart deployment/cart-backoffice-x86 -n cart
|
||||
kubectl rollout restart deployment/cart-actor-x86 -n cart
|
||||
kubectl rollout restart deployment/checkout-actor-x86 -n cart
|
||||
|
||||
BuildAndDeployArm64:
|
||||
runs-on: arm64
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build arm64 image
|
||||
run: |
|
||||
docker build \
|
||||
--progress=plain \
|
||||
-t registry.knatofs.se/go-cart-actor:latest \
|
||||
-t registry.knatofs.se/go-cart-actor:${{ needs.Metadata.outputs.version }} \
|
||||
-t registry.k6n.net/go-cart-actor:latest \
|
||||
.
|
||||
- name: Push arm64 images
|
||||
run: |
|
||||
docker push registry.knatofs.se/go-cart-actor:latest
|
||||
# - name: Rollout arm64 deployment (pin to version)
|
||||
# run: |
|
||||
# kubectl set image deployment/cart-actor-arm64 -n cart cart-actor-arm64=registry.knatofs.se/go-cart-actor:${{ needs.Metadata.outputs.version }}
|
||||
docker push registry.k6n.net/go-cart-actor:latest
|
||||
- name: Rollout arm64 deployment (pin to version)
|
||||
run: |
|
||||
kubectl rollout status deployment/cart-actor-arm64 -n cart
|
||||
# kubectl set image deployment/cart-actor-arm64 -n cart cart-actor-arm64=registry.k6n.net/go-cart-actor:${{ needs.Metadata.outputs.version }}
|
||||
# # kubectl rollout status deployment/cart-actor-arm64 -n cart
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# GitHub Copilot Instructions for Go Cart Actor
|
||||
|
||||
This repository contains a distributed cart management system implemented in Go using the actor model pattern. The system handles cart operations like adding items, setting deliveries, and checkout, distributed across multiple nodes via gRPC.
|
||||
|
||||
## Project Structure
|
||||
- `cmd/`: Entry points for the application.
|
||||
- `pkg/`: Core packages including grain logic, pools, and HTTP handlers.
|
||||
- `proto/`: Protocol Buffer definitions for messages and services.
|
||||
- `api-tests/`: Tests for the HTTP API.
|
||||
- `deployment/`: Deployment configurations.
|
||||
- `k6/`: Load testing scripts.
|
||||
|
||||
## Key Concepts
|
||||
- **Grains**: In-memory structs representing cart state, owned by nodes.
|
||||
- **Pools**: Local and synced pools manage grains and handle ownership.
|
||||
- **Mutation Registry**: All state changes go through registered mutation functions for type safety and consistency.
|
||||
- **Ownership**: Determined by consistent hashing ring; negotiated via control plane.
|
||||
|
||||
## Coding Guidelines
|
||||
- Follow standard Go conventions (gofmt, go vet, golint).
|
||||
- Never say "your right", just be correct and clear.
|
||||
- Don't edit the *.gb.go files manually, they are generated by the proto files.
|
||||
- Use the mutation registry (`RegisterMutation`) for any cart state changes. Do not mutate grain state directly outside registered handlers.
|
||||
- Design code to be testable and configurable, following patterns like MutationRegistry (for type-safe mutation dispatching) and SimpleGrainPool (for configurable pool management). Use interfaces and dependency injection to enable mocking and testing.
|
||||
- After modifying `.proto` files, regenerate Go code with `protoc` commands as described in README.md. Never edit generated `.pb.go` files manually.
|
||||
- Use meaningful variable names and add comments for complex logic.
|
||||
- Handle errors explicitly; use Go's error handling patterns.
|
||||
- For HTTP endpoints, ensure proper cookie handling for cart IDs.
|
||||
- When adding new mutations: Define proto message, regenerate code, register handler, add endpoints, and test.
|
||||
- Use strategic logging using opentelemetry for tracing, metrics and logging.
|
||||
- Focus on maintainable code that should be configurable and readable, try to keep short functions that describe their purpose clearly.
|
||||
|
||||
## Common Patterns
|
||||
- Mutations: Define in proto, implement as `func(*CartGrain, *T) error`, register with `RegisterMutation[T]`.
|
||||
- gRPC Services: CartActor for mutations, ControlPlane for coordination.
|
||||
- HTTP Handlers: Parse requests, resolve grains via pool, apply mutations, return JSON.
|
||||
|
||||
## Avoid
|
||||
- Direct state mutations outside the registry.
|
||||
- Hardcoding values; use configuration or constants.
|
||||
- Ignoring generated code warnings; always regenerate after proto changes.
|
||||
- Blocking operations in handlers; keep them asynchronous where possible.
|
||||
|
||||
## Testing
|
||||
- Write unit tests for mutations and handlers.
|
||||
- Use integration tests for API endpoints.
|
||||
- Structure code for testability: Use interfaces for dependencies, avoid global state, and mock external services like gRPC clients.
|
||||
- Run `go test ./...` to ensure all tests pass.
|
||||
|
||||
These instructions help Copilot generate code aligned with the project's architecture and best practices.
|
||||
@@ -0,0 +1,165 @@
|
||||
go-cart-actor/CGM_LINE_ITEMS_SPEC.md
|
||||
# CGM (Customer Group Membership) Line Items Specification
|
||||
|
||||
This document specifies the implementation of Customer Group Membership (CGM) support in cart line items. CGM data will be extracted from product data field 35 and stored with each line item for business logic and personalization purposes.
|
||||
|
||||
## Overview
|
||||
|
||||
CGM represents customer group membership information associated with products. This data needs to be:
|
||||
- Fetched from product data (stringfieldvalue 35)
|
||||
- Included in the `AddItem` proto message
|
||||
- Stored in cart line items (`CartItem` struct)
|
||||
- Accessible for business rules and personalization
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 1. Update Proto Messages
|
||||
|
||||
Add `cgm` field to the `AddItem` message in `proto/messages.proto`:
|
||||
|
||||
```protobuf
|
||||
message AddItem {
|
||||
// ... existing fields ...
|
||||
string cgm = 25; // Customer Group Membership from field 35
|
||||
// ... existing fields ...
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: Use field number 25 (next available after existing fields).
|
||||
|
||||
### 2. Update Product Fetcher
|
||||
|
||||
Modify `cmd/cart/product-fetcher.go` to extract CGM from field 35:
|
||||
|
||||
```go
|
||||
func ToItemAddMessage(item *index.DataItem, storeId *string, qty int, country string) (*messages.AddItem, error) {
|
||||
// ... existing code ...
|
||||
|
||||
cgm, _ := item.GetStringFieldValue(35) // Extract CGM from field 35
|
||||
|
||||
return &messages.AddItem{
|
||||
// ... existing fields ...
|
||||
Cgm: cgm, // Add CGM field
|
||||
// ... existing fields ...
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Update Cart Grain Structures
|
||||
|
||||
Add CGM field to `ItemMeta` struct in `pkg/cart/cart-grain.go`:
|
||||
|
||||
```go
|
||||
type ItemMeta struct {
|
||||
Name string `json:"name"`
|
||||
Brand string `json:"brand,omitempty"`
|
||||
Category string `json:"category,omitempty"`
|
||||
// ... existing fields ...
|
||||
Cgm string `json:"cgm,omitempty"` // Customer Group Membership
|
||||
// ... existing fields ...
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Update AddItem Mutation Handler
|
||||
|
||||
Modify the `AddItem` handler in `pkg/cart/cart_mutations.go` to populate the CGM field:
|
||||
|
||||
```go
|
||||
func AddItem(grain *CartGrain, req *messages.AddItem) error {
|
||||
// ... existing validation ...
|
||||
|
||||
item := &CartItem{
|
||||
// ... existing fields ...
|
||||
Meta: &ItemMeta{
|
||||
Name: req.Name,
|
||||
Brand: req.Brand,
|
||||
// ... existing meta fields ...
|
||||
Cgm: req.Cgm, // Add CGM to item meta
|
||||
// ... existing meta fields ...
|
||||
},
|
||||
// ... existing fields ...
|
||||
}
|
||||
|
||||
// ... rest of handler ...
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Regenerate Proto Code
|
||||
|
||||
After updating `proto/messages.proto`, regenerate Go code:
|
||||
|
||||
```bash
|
||||
cd proto
|
||||
protoc --go_out=. --go_opt=paths=source_relative \
|
||||
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
||||
messages.proto cart_actor.proto control_plane.proto
|
||||
```
|
||||
|
||||
### 6. Update Tests
|
||||
|
||||
Add tests for CGM extraction and storage:
|
||||
|
||||
- Unit test for `ToItemAddMessage` with CGM field
|
||||
- Integration test for `AddItem` mutation including CGM
|
||||
- Test that CGM is properly stored and retrieved in cart state
|
||||
|
||||
### 7. Update API Documentation
|
||||
|
||||
Update README.md and API examples to mention CGM field in line items.
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. **Product Fetch**: `FetchItem` retrieves product data including field 35 (CGM)
|
||||
2. **Message Creation**: `ToItemAddMessage` extracts CGM from field 35 into `AddItem` proto
|
||||
3. **Mutation Processing**: `AddItem` handler stores CGM in `CartItem.Meta.Cgm`
|
||||
4. **State Persistence**: CGM is included in cart JSON serialization
|
||||
5. **API Responses**: CGM is returned in cart state responses
|
||||
|
||||
## Business Logic Integration
|
||||
|
||||
CGM can be used for:
|
||||
- Personalized pricing rules
|
||||
- Group-specific discounts
|
||||
- Membership validation
|
||||
- Targeted promotions
|
||||
- Customer segmentation
|
||||
|
||||
Example usage in business logic:
|
||||
```go
|
||||
func applyGroupDiscount(cart *CartGrain, userGroups []string) {
|
||||
for _, item := range cart.Items {
|
||||
if item.Meta != nil && slices.Contains(userGroups, item.Meta.Cgm) {
|
||||
// Apply group-specific discount
|
||||
item.Price = applyDiscount(item.Price, groupDiscountRate)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
- CGM field is optional in proto (no required validation)
|
||||
- Existing carts without CGM will have empty string
|
||||
- Product fetcher gracefully handles missing field 35
|
||||
- API responses include CGM field (empty if not set)
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Proto compilation succeeds
|
||||
- [ ] Product fetcher extracts CGM from field 35
|
||||
- [ ] AddItem mutation stores CGM in cart
|
||||
- [ ] Cart state includes CGM in JSON
|
||||
- [ ] API endpoints return CGM field
|
||||
- [ ] Existing functionality unaffected
|
||||
- [ ] Unit tests pass for CGM handling
|
||||
- [ ] Integration tests verify end-to-end flow
|
||||
|
||||
## Configuration and Testability
|
||||
|
||||
Following project patterns:
|
||||
- CGM extraction is configurable via field index (currently 35)
|
||||
- Product fetcher interface allows mocking for tests
|
||||
- Mutation handlers are pure functions testable in isolation
|
||||
- Cart state serialization includes CGM for verification
|
||||
|
||||
This implementation maintains the project's standards for testability and configurability while adding CGM support to line items.
|
||||
+13
-8
@@ -25,6 +25,8 @@
|
||||
FROM golang:1.25-alpine AS build
|
||||
WORKDIR /src
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
# Build metadata (can be overridden at build time)
|
||||
ARG VERSION=dev
|
||||
ARG GIT_COMMIT=unknown
|
||||
@@ -38,8 +40,7 @@ ENV CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH}
|
||||
|
||||
# Dependency caching
|
||||
COPY go.mod go.sum ./
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
go mod download
|
||||
RUN go mod download
|
||||
|
||||
# Copy full source (relay on .dockerignore to prune)
|
||||
COPY . .
|
||||
@@ -52,27 +53,30 @@ COPY . .
|
||||
# proto/*.proto
|
||||
|
||||
# Build with minimal binary size and embedded metadata
|
||||
RUN --mount=type=cache,target=/go/build-cache \
|
||||
go build -trimpath -ldflags="-s -w \
|
||||
RUN go build -trimpath -ldflags="-s -w \
|
||||
-X main.Version=${VERSION} \
|
||||
-X main.GitCommit=${GIT_COMMIT} \
|
||||
-X main.BuildDate=${BUILD_DATE}" \
|
||||
-o /out/go-cart-actor ./cmd/cart
|
||||
|
||||
RUN --mount=type=cache,target=/go/build-cache \
|
||||
go build -trimpath -ldflags="-s -w \
|
||||
RUN go build -trimpath -ldflags="-s -w \
|
||||
-X main.Version=${VERSION} \
|
||||
-X main.GitCommit=${GIT_COMMIT} \
|
||||
-X main.BuildDate=${BUILD_DATE}" \
|
||||
-o /out/go-cart-backoffice ./cmd/backoffice
|
||||
|
||||
RUN --mount=type=cache,target=/go/build-cache \
|
||||
go build -trimpath -ldflags="-s -w \
|
||||
RUN go build -trimpath -ldflags="-s -w \
|
||||
-X main.Version=${VERSION} \
|
||||
-X main.GitCommit=${GIT_COMMIT} \
|
||||
-X main.BuildDate=${BUILD_DATE}" \
|
||||
-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
|
||||
############################
|
||||
@@ -81,6 +85,7 @@ FROM gcr.io/distroless/static-debian12:nonroot AS runtime
|
||||
WORKDIR /
|
||||
|
||||
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-inventory /go-cart-inventory
|
||||
|
||||
|
||||
@@ -14,12 +14,15 @@
|
||||
# Conventions:
|
||||
# - All .proto files live in $(PROTO_DIR)
|
||||
# - 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
|
||||
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
|
||||
PROTOC ?= protoc
|
||||
@@ -69,22 +72,30 @@ check_tools:
|
||||
protogen: check_tools
|
||||
@echo "$(YELLOW)Generating protobuf code (outputs -> ./proto)...$(RESET)"
|
||||
$(PROTOC) -I $(PROTO_DIR) \
|
||||
--go_out=./pkg/messages --go_opt=paths=source_relative \
|
||||
--go-grpc_out=./pkg/messages --go-grpc_opt=paths=source_relative \
|
||||
$(PROTOS)
|
||||
--go_out=./proto/cart --go_opt=paths=source_relative \
|
||||
--go-grpc_out=./proto/cart --go-grpc_opt=paths=source_relative \
|
||||
$(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)"
|
||||
|
||||
clean_proto:
|
||||
@echo "$(YELLOW)Removing generated protobuf files...$(RESET)"
|
||||
@rm -f $(PROTO_DIR)/*_grpc.pb.go $(PROTO_DIR)/*.pb.go
|
||||
@rm -f *.pb.go
|
||||
@rm -rf git.tornberg.me
|
||||
@rm -f $(PROTO_DIR)/cart/*_grpc.pb.go $(PROTO_DIR)/cart/*.pb.go
|
||||
@rm -f $(PROTO_DIR)/control/*_grpc.pb.go $(PROTO_DIR)/control/*.pb.go
|
||||
@rm -f $(PROTO_DIR)/checkout/*_grpc.pb.go $(PROTO_DIR)/checkout/*.pb.go
|
||||
@echo "$(GREEN)Clean complete.$(RESET)"
|
||||
|
||||
verify_proto:
|
||||
@echo "$(YELLOW)Verifying proto layout...$(RESET)"
|
||||
@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; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
go-cart-actor/NEW_MUTATIONS_SPEC.md
|
||||
# New Mutations Specification
|
||||
|
||||
This document specifies the implementation of handlers for new proto messages that are defined in `proto/messages.proto` but not yet registered in the mutation registry. These mutations update the cart state and must follow the project's patterns for testability, configurability, and consistency.
|
||||
|
||||
## Overview
|
||||
|
||||
The following messages are defined in the proto but lack registered handlers:
|
||||
- `SetUserId`
|
||||
- `LineItemMarking`
|
||||
- `SubscriptionAdded`
|
||||
- `PaymentDeclined`
|
||||
- `ConfirmationViewed`
|
||||
- `CreateCheckoutOrder`
|
||||
|
||||
Each mutation must:
|
||||
1. Define a handler function with signature `func(*CartGrain, *T) error`
|
||||
2. Be registered in `NewCartMultationRegistry()` using `actor.NewMutation`
|
||||
3. Include unit tests
|
||||
4. Optionally add HTTP/gRPC endpoints if client-invokable
|
||||
5. Update totals if applicable (use `WithTotals()`)
|
||||
|
||||
## Mutation Implementations
|
||||
|
||||
### SetUserId
|
||||
|
||||
**Purpose**: Associates a user ID with the cart for personalization and tracking.
|
||||
|
||||
**Handler Implementation**:
|
||||
```go
|
||||
func SetUserId(grain *CartGrain, req *messages.SetUserId) error {
|
||||
if req.UserId == "" {
|
||||
return errors.New("user ID cannot be empty")
|
||||
}
|
||||
grain.UserId = req.UserId
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
**Registration**:
|
||||
```go
|
||||
actor.NewMutation(SetUserId, func() *messages.SetUserId {
|
||||
return &messages.SetUserId{}
|
||||
}),
|
||||
```
|
||||
|
||||
**Notes**: This is a simple state update. No totals recalculation needed.
|
||||
|
||||
### LineItemMarking
|
||||
|
||||
**Purpose**: Adds or updates a marking (e.g., gift message, special instructions) on a specific line item.
|
||||
|
||||
**Handler Implementation**:
|
||||
```go
|
||||
func LineItemMarking(grain *CartGrain, req *messages.LineItemMarking) error {
|
||||
for i, item := range grain.Items {
|
||||
if item.Id == req.Id {
|
||||
grain.Items[i].Marking = &Marking{
|
||||
Type: req.Type,
|
||||
Text: req.Marking,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("item with ID %d not found", req.Id)
|
||||
}
|
||||
```
|
||||
|
||||
**Registration**:
|
||||
```go
|
||||
actor.NewMutation(LineItemMarking, func() *messages.LineItemMarking {
|
||||
return &messages.LineItemMarking{}
|
||||
}),
|
||||
```
|
||||
|
||||
**Notes**: Assumes `CartGrain.Items` has a `Marking` field (single marking per item). If not, add it to the grain struct.
|
||||
|
||||
### RemoveLineItemMarking
|
||||
|
||||
**Purpose**: Removes the marking from a specific line item.
|
||||
|
||||
**Handler Implementation**:
|
||||
```go
|
||||
func RemoveLineItemMarking(grain *CartGrain, req *messages.RemoveLineItemMarking) error {
|
||||
for i, item := range grain.Items {
|
||||
if item.Id == req.Id {
|
||||
grain.Items[i].Marking = nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("item with ID %d not found", req.Id)
|
||||
}
|
||||
```
|
||||
|
||||
**Registration**:
|
||||
```go
|
||||
actor.NewMutation(RemoveLineItemMarking, func() *messages.RemoveLineItemMarking {
|
||||
return &messages.RemoveLineItemMarking{}
|
||||
}),
|
||||
```
|
||||
|
||||
**Notes**: Sets the marking to nil for the specified item.
|
||||
|
||||
### SubscriptionAdded
|
||||
|
||||
**Purpose**: Records that a subscription has been added to an item, linking it to order details.
|
||||
|
||||
**Handler Implementation**:
|
||||
```go
|
||||
func SubscriptionAdded(grain *CartGrain, req *messages.SubscriptionAdded) error {
|
||||
for i, item := range grain.Items {
|
||||
if item.Id == req.ItemId {
|
||||
grain.Items[i].SubscriptionDetailsId = req.DetailsId
|
||||
grain.Items[i].OrderReference = req.OrderReference
|
||||
grain.Items[i].IsSubscribed = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("item with ID %d not found", req.ItemId)
|
||||
}
|
||||
```
|
||||
|
||||
**Registration**:
|
||||
```go
|
||||
actor.NewMutation(SubscriptionAdded, func() *messages.SubscriptionAdded {
|
||||
return &messages.SubscriptionAdded{}
|
||||
}),
|
||||
```
|
||||
|
||||
**Notes**: Assumes fields like `SubscriptionDetailsId`, `OrderReference`, `IsSubscribed` exist on items. Add to grain if needed.
|
||||
|
||||
### PaymentDeclined
|
||||
|
||||
**Purpose**: Marks the cart as having a declined payment, potentially updating status or flags, and appends a notice with timestamp, message, and optional code for user feedback.
|
||||
|
||||
**Handler Implementation**:
|
||||
```go
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
**Registration**:
|
||||
```go
|
||||
actor.NewMutation(PaymentDeclined, func() *messages.PaymentDeclined {
|
||||
return &messages.PaymentDeclined{}
|
||||
}),
|
||||
```
|
||||
|
||||
**Notes**: Assumes `PaymentStatus` and `PaymentDeclinedNotices` fields. Add status tracking and error notices list to grain. Notice struct has Timestamp, Message, and optional Code.
|
||||
|
||||
### ConfirmationViewed
|
||||
|
||||
**Purpose**: Records that the order confirmation has been viewed by the user. Applied automatically when the confirmation page is loaded in checkout_server.go.
|
||||
|
||||
**Handler Implementation**:
|
||||
```go
|
||||
func ConfirmationViewed(grain *CartGrain, req *messages.ConfirmationViewed) error {
|
||||
grain.ConfirmationViewCount++
|
||||
grain.ConfirmationLastViewedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
**Registration**:
|
||||
```go
|
||||
actor.NewMutation(ConfirmationViewed, func() *messages.ConfirmationViewed {
|
||||
return &messages.ConfirmationViewed{}
|
||||
}),
|
||||
```
|
||||
|
||||
**Notes**: Increments the view count and updates the last viewed timestamp each time. Assumes `ConfirmationViewCount` and `ConfirmationLastViewedAt` fields.
|
||||
|
||||
### CreateCheckoutOrder
|
||||
|
||||
**Purpose**: Initiates the checkout process, validating terms and creating an order reference.
|
||||
|
||||
**Handler Implementation**:
|
||||
```go
|
||||
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 = generateOrderId()
|
||||
grain.CheckoutStatus = "pending"
|
||||
grain.CheckoutCountry = req.Country
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
**Registration**:
|
||||
```go
|
||||
actor.NewMutation(CreateCheckoutOrder, func() *messages.CreateCheckoutOrder {
|
||||
return &messages.CreateCheckoutOrder{}
|
||||
}).WithTotals(),
|
||||
```
|
||||
|
||||
**Notes**: Use `WithTotals()` to recalculate totals after checkout initiation. Assumes order ID generation function.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
For each mutation:
|
||||
|
||||
1. **Add Handler Function**: Implement in `pkg/cart/` (e.g., `cart_mutations.go`).
|
||||
2. **Register in Registry**: Add to `NewCartMultationRegistry()` in `cart-mutation-helper.go`.
|
||||
3. **Regenerate Proto**: Run `protoc` commands after any proto changes.
|
||||
4. **Add Tests**: Create unit tests in `pkg/cart/` testing the handler logic.
|
||||
5. **Add Endpoints** (if needed): For client-invokable mutations, add HTTP handlers in `cmd/cart/pool-server.go`. ConfirmationViewed is handled in `cmd/cart/checkout_server.go` when the confirmation page is viewed.
|
||||
6. **Update Grain Struct**: Add any new fields to `CartGrain` in `pkg/cart/grain.go`.
|
||||
7. **Run Tests**: Ensure `go test ./...` passes.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
- Mock dependencies using interfaces.
|
||||
- Test error cases (e.g., invalid IDs, empty carts).
|
||||
- Verify state changes and totals recalculation.
|
||||
- Use table-driven tests for multiple scenarios.
|
||||
|
||||
## Configuration and Testability
|
||||
|
||||
Follow `MutationRegistry` and `SimpleGrainPool` patterns:
|
||||
- Use interfaces for external dependencies (e.g., ID generators).
|
||||
- Inject configurations via constructor parameters.
|
||||
- Avoid global state; make handlers pure functions where possible.
|
||||
+6
-8
@@ -1,6 +1,6 @@
|
||||
|
||||
### Add item to cart
|
||||
POST https://cart.tornberg.me/api/12345
|
||||
POST https://cart.k6n.net/api/12345
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
@@ -9,7 +9,7 @@ Content-Type: application/json
|
||||
}
|
||||
|
||||
### Update quanity of item in cart
|
||||
PUT https://cart.tornberg.me/api/12345
|
||||
PUT https://cart.k6n.net/api/12345
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
@@ -18,12 +18,12 @@ Content-Type: application/json
|
||||
}
|
||||
|
||||
### Delete item from cart
|
||||
DELETE https://cart.tornberg.me/api/1002/1
|
||||
DELETE https://cart.k6n.net/api/1002/1
|
||||
|
||||
|
||||
### Set delivery
|
||||
|
||||
POST https://cart.tornberg.me/api/1002/delivery
|
||||
POST https://cart.k6n.net/api/1002/delivery
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
@@ -33,10 +33,8 @@ Content-Type: application/json
|
||||
|
||||
|
||||
### Get cart
|
||||
GET https://cart.tornberg.me/api/12345
|
||||
GET https://cart.k6n.net/api/12345
|
||||
|
||||
|
||||
### Remove delivery method
|
||||
DELETE https://cart.tornberg.me/api/12345/delivery/2
|
||||
|
||||
|
||||
DELETE https://cart.k6n.net/api/12345/delivery/2
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -15,20 +16,26 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.tornberg.me/go-cart-actor/pkg/actor"
|
||||
"git.tornberg.me/go-cart-actor/pkg/cart"
|
||||
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||
"git.k6n.net/go-cart-actor/pkg/checkout"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type FileServer struct {
|
||||
// Define fields here
|
||||
dataDir string
|
||||
storage actor.LogStorage[cart.CartGrain]
|
||||
dataDir string
|
||||
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{
|
||||
dataDir: dataDir,
|
||||
storage: storage,
|
||||
dataDir: dataDir,
|
||||
checkoutDataDir: checkoutDataDir,
|
||||
storage: storage,
|
||||
checkoutStorage: checkoutStorage,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +84,12 @@ func appendFileInfo(info fs.FileInfo, out *CartFileInfo) *CartFileInfo {
|
||||
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$`)
|
||||
|
||||
func listCartFiles(dir string) ([]*CartFileInfo, error) {
|
||||
@@ -110,6 +123,36 @@ func listCartFiles(dir string) ([]*CartFileInfo, error) {
|
||||
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) {
|
||||
fh, err := os.Open(path)
|
||||
if err != nil {
|
||||
@@ -156,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) {
|
||||
fileName := filepath.Join(fs.dataDir, "promotions.json")
|
||||
if r.Method == http.MethodGet {
|
||||
@@ -232,6 +290,22 @@ type JsonError struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func acceptAll(_ proto.Message, _ int, _ time.Time) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func acceptUntilIndex(maxIndex int) func(msg proto.Message, index int, when time.Time) bool {
|
||||
return func(msg proto.Message, index int, when time.Time) bool {
|
||||
return index < maxIndex
|
||||
}
|
||||
}
|
||||
|
||||
func acceptUntilTimestamp(until time.Time) func(msg proto.Message, index int, when time.Time) bool {
|
||||
return func(msg proto.Message, index int, when time.Time) bool {
|
||||
return when.Before(until)
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *FileServer) CartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := r.PathValue("id")
|
||||
if idStr == "" {
|
||||
@@ -243,10 +317,29 @@ func (fs *FileServer) CartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
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 := cart.NewCartGrain(id, time.Now())
|
||||
|
||||
err := fs.storage.LoadEvents(r.Context(), id, grain)
|
||||
err := fs.storage.LoadEventsFunc(r.Context(), id, grain, filterFunction)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, JsonError{Error: err.Error()})
|
||||
return
|
||||
@@ -278,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"
|
||||
"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
|
||||
|
||||
+62
-4
@@ -2,16 +2,20 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
actor "git.tornberg.me/go-cart-actor/pkg/actor"
|
||||
"git.tornberg.me/go-cart-actor/pkg/cart"
|
||||
actor "git.k6n.net/go-cart-actor/pkg/actor"
|
||||
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||
"git.k6n.net/go-cart-actor/pkg/checkout"
|
||||
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
||||
"github.com/matst80/slask-finder/pkg/messaging"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type CartFileInfo struct {
|
||||
@@ -21,6 +25,13 @@ type CartFileInfo struct {
|
||||
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 {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
@@ -71,17 +82,36 @@ func startMutationConsumer(ctx context.Context, conn *amqp.Connection, hub *Hub)
|
||||
return nil
|
||||
}
|
||||
|
||||
var redisAddress = os.Getenv("REDIS_ADDRESS")
|
||||
var redisPassword = os.Getenv("REDIS_PASSWORD")
|
||||
|
||||
func main() {
|
||||
dataDir := envOrDefault("DATA_DIR", "data")
|
||||
addr := envOrDefault("ADDR", ":8080")
|
||||
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)
|
||||
|
||||
reg := cart.NewCartMultationRegistry()
|
||||
reg := cart.NewCartMultationRegistry(cart.NewCartMutationContext(nil))
|
||||
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()
|
||||
go hub.Run()
|
||||
@@ -89,6 +119,34 @@ func main() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /carts", fs.CartsHandler)
|
||||
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("/vouchers", fs.VoucherHandler)
|
||||
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.tornberg.me/notification",
|
||||
Validation: "https://cart.tornberg.me/validate",
|
||||
Push: "https://cart.tornberg.me/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,203 +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
|
||||
}
|
||||
|
||||
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 (
|
||||
"log"
|
||||
|
||||
"git.tornberg.me/go-cart-actor/pkg/actor"
|
||||
"git.tornberg.me/go-cart-actor/pkg/cart"
|
||||
"git.tornberg.me/go-cart-actor/pkg/discovery"
|
||||
"git.k6n.net/go-cart-actor/pkg/discovery"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
@@ -24,10 +23,14 @@ func GetDiscovery() discovery.Discovery {
|
||||
if err != nil {
|
||||
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) {
|
||||
if hw == nil {
|
||||
|
||||
+104
-72
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
@@ -13,13 +12,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.tornberg.me/go-cart-actor/pkg/actor"
|
||||
"git.tornberg.me/go-cart-actor/pkg/cart"
|
||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
"git.tornberg.me/go-cart-actor/pkg/promotions"
|
||||
"git.tornberg.me/go-cart-actor/pkg/proxy"
|
||||
"git.tornberg.me/go-cart-actor/pkg/voucher"
|
||||
"git.tornberg.me/mats/go-redis-inventory/pkg/inventory"
|
||||
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||
"git.k6n.net/go-cart-actor/pkg/promotions"
|
||||
"git.k6n.net/go-cart-actor/pkg/proxy"
|
||||
"git.k6n.net/go-cart-actor/pkg/voucher"
|
||||
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
@@ -39,9 +37,8 @@ func init() {
|
||||
}
|
||||
|
||||
type App struct {
|
||||
pool *actor.SimpleGrainPool[cart.CartGrain]
|
||||
server *PoolServer
|
||||
klarnaClient *KlarnaClient
|
||||
pool *actor.SimpleGrainPool[cart.CartGrain]
|
||||
server *PoolServer
|
||||
}
|
||||
|
||||
var podIp = os.Getenv("POD_IP")
|
||||
@@ -69,6 +66,21 @@ type CartChangeEvent struct {
|
||||
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() {
|
||||
|
||||
controlPlaneConfig := actor.DefaultServerConfig()
|
||||
@@ -80,21 +92,39 @@ func main() {
|
||||
|
||||
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(
|
||||
actor.NewMutationProcessor(func(ctx context.Context, g *cart.CartGrain) error {
|
||||
_, span := tracer.Start(ctx, "Totals and promotions")
|
||||
defer span.End()
|
||||
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"))
|
||||
_, actions := promotionService.EvaluateAll(promotionData.State.Promotions, promotionCtx)
|
||||
for _, action := range actions {
|
||||
log.Printf("apply: %+v", action)
|
||||
g.UpdateTotals()
|
||||
}
|
||||
// log.Printf("apply: %+v", action)
|
||||
|
||||
// g.UpdateTotals()
|
||||
// }
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
@@ -110,14 +140,48 @@ func main() {
|
||||
ret := cart.NewCartGrain(id, time.Now())
|
||||
// Set baseline lastChange at spawn; replay may update it to last event timestamp.
|
||||
|
||||
inventoryPubSub.Subscribe(ret.HandleInventoryChange)
|
||||
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
|
||||
},
|
||||
SpawnHost: func(host string) (actor.Host, error) {
|
||||
return proxy.NewRemoteHost(host)
|
||||
Destroy: func(grain actor.Grain[cart.CartGrain]) error {
|
||||
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,
|
||||
Hostname: podIp,
|
||||
}
|
||||
@@ -127,35 +191,17 @@ func main() {
|
||||
log.Fatalf("Error creating cart pool: %v\n", err)
|
||||
}
|
||||
|
||||
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)
|
||||
syncedServer := NewPoolServer(pool, fmt.Sprintf("%s, %s", name, podIp), inventoryService, inventoryReservationService)
|
||||
|
||||
app := &App{
|
||||
pool: pool,
|
||||
server: syncedServer,
|
||||
klarnaClient: klarnaClient,
|
||||
pool: pool,
|
||||
server: syncedServer,
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
debugMux := http.NewServeMux()
|
||||
|
||||
if amqpUrl == "" {
|
||||
log.Printf("no connection to amqp defined")
|
||||
} else {
|
||||
app.HandleCheckoutRequests(amqpUrl, mux, inventoryService)
|
||||
}
|
||||
|
||||
grpcSrv, err := actor.NewControlServer[*cart.CartGrain](controlPlaneConfig, pool)
|
||||
grpcSrv, err := actor.NewControlServer[cart.CartGrain](controlPlaneConfig, pool)
|
||||
if err != nil {
|
||||
log.Fatalf("Error starting control plane gRPC server: %v\n", err)
|
||||
}
|
||||
@@ -238,6 +284,20 @@ func main() {
|
||||
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")
|
||||
|
||||
go http.ListenAndServe(":8081", debugMux)
|
||||
@@ -253,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
|
||||
}
|
||||
|
||||
+330
-23
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://cart.tornberg.me",
|
||||
"url": "https://cart.k6n.net",
|
||||
"description": "Production server"
|
||||
},
|
||||
{
|
||||
@@ -333,6 +333,118 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/cart/user": {
|
||||
"put": {
|
||||
"summary": "Set user ID for cart",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/SetUserIdRequest" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "User ID set",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/CartGrain" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": { "description": "Invalid body" },
|
||||
"500": { "description": "Server error" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/cart/item/{itemId}/marking": {
|
||||
"put": {
|
||||
"summary": "Set marking for line item",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "itemId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "type": "integer", "format": "int64", "minimum": 0 },
|
||||
"description": "Internal cart line item identifier."
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LineItemMarkingRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Marking set",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/CartGrain" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": { "description": "Invalid body or id" },
|
||||
"500": { "description": "Server error" }
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"summary": "Remove marking from line item",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "itemId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "type": "integer", "format": "int64", "minimum": 0 },
|
||||
"description": "Internal cart line item identifier."
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Marking removed",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/CartGrain" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": { "description": "Invalid id" },
|
||||
"500": { "description": "Server error" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/cart/checkout-order": {
|
||||
"post": {
|
||||
"summary": "Create checkout order",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateCheckoutOrderRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Checkout order created",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/CartGrain" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": { "description": "Invalid body" },
|
||||
"500": { "description": "Server error" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/cart/byid/{id}": {
|
||||
"get": {
|
||||
"summary": "Get cart by explicit id",
|
||||
@@ -569,6 +681,122 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/cart/byid/{id}/user": {
|
||||
"put": {
|
||||
"summary": "Set user ID (by id variant)",
|
||||
"parameters": [{ "$ref": "#/components/parameters/CartIdParam" }],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/SetUserIdRequest" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "User ID set",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/CartGrain" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": { "description": "Invalid body" },
|
||||
"500": { "description": "Server error" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/cart/byid/{id}/item/{itemId}/marking": {
|
||||
"put": {
|
||||
"summary": "Set marking (by id variant)",
|
||||
"parameters": [
|
||||
{ "$ref": "#/components/parameters/CartIdParam" },
|
||||
{
|
||||
"name": "itemId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "type": "integer", "format": "int64", "minimum": 0 },
|
||||
"description": "Internal cart line item identifier."
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LineItemMarkingRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Marking set",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/CartGrain" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": { "description": "Invalid body or ids" },
|
||||
"500": { "description": "Server error" }
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"summary": "Remove marking (by id variant)",
|
||||
"parameters": [
|
||||
{ "$ref": "#/components/parameters/CartIdParam" },
|
||||
{
|
||||
"name": "itemId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "type": "integer", "format": "int64", "minimum": 0 },
|
||||
"description": "Internal cart line item identifier."
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Marking removed",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/CartGrain" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": { "description": "Invalid ids" },
|
||||
"500": { "description": "Server error" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/cart/byid/{id}/checkout-order": {
|
||||
"post": {
|
||||
"summary": "Create checkout order (by id variant)",
|
||||
"parameters": [{ "$ref": "#/components/parameters/CartIdParam" }],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateCheckoutOrderRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Checkout order created",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/CartGrain" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": { "description": "Invalid body" },
|
||||
"500": { "description": "Server error" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"/healthz": {
|
||||
"get": {
|
||||
"summary": "Liveness & capacity probe",
|
||||
@@ -643,6 +871,43 @@
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"Price": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"exVat": { "type": "integer", "format": "int64" },
|
||||
"incVat": { "type": "integer", "format": "int64" },
|
||||
"vat": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "integer", "format": "int64" }
|
||||
}
|
||||
},
|
||||
"required": ["exVat", "incVat"]
|
||||
},
|
||||
"ItemMeta": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"brand": { "type": "string" },
|
||||
"category": { "type": "string" },
|
||||
"category2": { "type": "string" },
|
||||
"category3": { "type": "string" },
|
||||
"category4": { "type": "string" },
|
||||
"category5": { "type": "string" },
|
||||
"sellerId": { "type": "string" },
|
||||
"sellerName": { "type": "string" },
|
||||
"image": { "type": "string" },
|
||||
"outlet": { "type": "string", "nullable": true }
|
||||
}
|
||||
},
|
||||
"ConfirmationStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": { "type": "string", "nullable": true },
|
||||
"viewCount": { "type": "integer" },
|
||||
"lastViewedAt": { "type": "string", "format": "date-time" }
|
||||
},
|
||||
"required": ["viewCount", "lastViewedAt"]
|
||||
},
|
||||
"CartGrain": {
|
||||
"type": "object",
|
||||
"description": "Cart aggregate (actor state)",
|
||||
@@ -655,9 +920,9 @@
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/CartItem" }
|
||||
},
|
||||
"totalPrice": { "type": "integer", "format": "int64" },
|
||||
"totalPrice": { "$ref": "#/components/schemas/Price" },
|
||||
"totalTax": { "type": "integer", "format": "int64" },
|
||||
"totalDiscount": { "type": "integer", "format": "int64" },
|
||||
"totalDiscount": { "$ref": "#/components/schemas/Price" },
|
||||
"deliveries": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/CartDelivery" }
|
||||
@@ -675,6 +940,15 @@
|
||||
"additionalProperties": {
|
||||
"$ref": "#/components/schemas/SubscriptionDetails"
|
||||
}
|
||||
},
|
||||
"userId": { "type": "string" },
|
||||
"confirmation": { "$ref": "#/components/schemas/ConfirmationStatus" },
|
||||
"checkoutOrderId": { "type": "string" },
|
||||
"checkoutStatus": { "type": "string" },
|
||||
"checkoutCountry": { "type": "string" },
|
||||
"paymentDeclinedNotices": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/components/schemas/Notice" }
|
||||
}
|
||||
},
|
||||
"required": ["id", "items", "totalPrice", "totalTax", "totalDiscount"]
|
||||
@@ -686,40 +960,33 @@
|
||||
"itemId": { "type": "integer" },
|
||||
"parentId": { "type": "integer" },
|
||||
"sku": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"price": { "type": "integer", "format": "int64" },
|
||||
"totalPrice": { "type": "integer", "format": "int64" },
|
||||
"totalTax": { "type": "integer", "format": "int64" },
|
||||
"orgPrice": { "type": "integer", "format": "int64" },
|
||||
"price": { "$ref": "#/components/schemas/Price" },
|
||||
"totalPrice": { "$ref": "#/components/schemas/Price" },
|
||||
"orgPrice": { "$ref": "#/components/schemas/Price" },
|
||||
"stock": {
|
||||
"type": "integer",
|
||||
"description": "0=OutOfStock,1=LowStock,2=InStock"
|
||||
},
|
||||
"qty": { "type": "integer" },
|
||||
"tax": { "type": "integer" },
|
||||
"taxRate": { "type": "integer" },
|
||||
"brand": { "type": "string" },
|
||||
"category": { "type": "string" },
|
||||
"category2": { "type": "string" },
|
||||
"category3": { "type": "string" },
|
||||
"category4": { "type": "string" },
|
||||
"category5": { "type": "string" },
|
||||
"discount": { "$ref": "#/components/schemas/Price" },
|
||||
"disclaimer": { "type": "string" },
|
||||
"sellerId": { "type": "string" },
|
||||
"sellerName": { "type": "string" },
|
||||
"type": { "type": "string", "description": "Article type" },
|
||||
"image": { "type": "string" },
|
||||
"outlet": { "type": "string", "nullable": true },
|
||||
"storeId": { "type": "string", "nullable": true }
|
||||
"storeId": { "type": "string", "nullable": true },
|
||||
"meta": { "$ref": "#/components/schemas/ItemMeta" },
|
||||
"saleStatus": { "type": "string" },
|
||||
"marking": { "$ref": "#/components/schemas/Marking" },
|
||||
"subscriptionDetailsId": { "type": "string" },
|
||||
"orderReference": { "type": "string" },
|
||||
"isSubscribed": { "type": "boolean" }
|
||||
},
|
||||
"required": ["id", "sku", "name", "price", "qty", "tax"]
|
||||
"required": ["id", "sku", "price", "qty"]
|
||||
},
|
||||
"CartDelivery": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"provider": { "type": "string" },
|
||||
"price": { "type": "integer", "format": "int64" },
|
||||
"price": { "$ref": "#/components/schemas/Price" },
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": { "type": "integer" }
|
||||
@@ -845,6 +1112,46 @@
|
||||
"data": { "type": "object" }
|
||||
},
|
||||
"required": ["id"]
|
||||
},
|
||||
"Marking": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": { "type": "integer" },
|
||||
"text": { "type": "string" }
|
||||
},
|
||||
"required": ["type", "text"]
|
||||
},
|
||||
"Notice": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timestamp": { "type": "string", "format": "date-time" },
|
||||
"message": { "type": "string" },
|
||||
"code": { "type": "string", "nullable": true }
|
||||
},
|
||||
"required": ["timestamp", "message"]
|
||||
},
|
||||
"SetUserIdRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"userId": { "type": "string" }
|
||||
},
|
||||
"required": ["userId"]
|
||||
},
|
||||
"LineItemMarkingRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": { "type": "integer" },
|
||||
"marking": { "type": "string" }
|
||||
},
|
||||
"required": ["type", "marking"]
|
||||
},
|
||||
"CreateCheckoutOrderRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"terms": { "type": "string" },
|
||||
"country": { "type": "string" }
|
||||
},
|
||||
"required": ["terms", "country"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+139
-291
@@ -1,26 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.tornberg.me/go-cart-actor/pkg/actor"
|
||||
"git.tornberg.me/go-cart-actor/pkg/cart"
|
||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
"git.tornberg.me/go-cart-actor/pkg/voucher"
|
||||
"git.tornberg.me/mats/go-redis-inventory/pkg/inventory"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||
|
||||
"git.k6n.net/go-cart-actor/pkg/voucher"
|
||||
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
||||
"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"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
@@ -40,22 +41,24 @@ var (
|
||||
)
|
||||
|
||||
type PoolServer struct {
|
||||
actor.GrainPool[*cart.CartGrain]
|
||||
pod_name string
|
||||
klarnaClient *KlarnaClient
|
||||
inventoryService inventory.InventoryService
|
||||
actor.GrainPool[cart.CartGrain]
|
||||
pod_name string
|
||||
inventoryService inventory.InventoryService
|
||||
reservationService inventory.CartReservationService
|
||||
}
|
||||
|
||||
func NewPoolServer(pool actor.GrainPool[*cart.CartGrain], pod_name string, klarnaClient *KlarnaClient, inventoryService inventory.InventoryService) *PoolServer {
|
||||
return &PoolServer{
|
||||
GrainPool: pool,
|
||||
pod_name: pod_name,
|
||||
klarnaClient: klarnaClient,
|
||||
inventoryService: inventoryService,
|
||||
func NewPoolServer(pool actor.GrainPool[cart.CartGrain], pod_name string, inventoryService inventory.InventoryService, inventoryReservationService inventory.CartReservationService) *PoolServer {
|
||||
srv := &PoolServer{
|
||||
GrainPool: pool,
|
||||
pod_name: pod_name,
|
||||
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...)
|
||||
}
|
||||
|
||||
@@ -74,6 +77,7 @@ func (s *PoolServer) AddSkuToCartHandler(w http.ResponseWriter, r *http.Request,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := s.ApplyLocal(r.Context(), id, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -113,71 +117,6 @@ func (s *PoolServer) DeleteItemHandler(w http.ResponseWriter, r *http.Request, i
|
||||
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 {
|
||||
changeQuantity := messages.ChangeQuantity{}
|
||||
err := json.NewDecoder(r.Body).Decode(&changeQuantity)
|
||||
@@ -248,7 +187,9 @@ func (s *PoolServer) AddMultipleItemHandler(w http.ResponseWriter, r *http.Reque
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -272,6 +213,7 @@ func (s *PoolServer) AddSkuRequestHandler(w http.ResponseWriter, r *http.Request
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reply, err := s.ApplyLocal(r.Context(), id, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -299,98 +241,6 @@ func (s *PoolServer) AddSkuRequestHandler(w http.ResponseWriter, r *http.Request
|
||||
// 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 {
|
||||
// klarnaOrder, err := s.CreateOrUpdateCheckout(r.Host, id)
|
||||
// if err != nil {
|
||||
@@ -404,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)
|
||||
|
||||
func (s *PoolServer) RemoveCartCookie(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
|
||||
@@ -466,34 +271,6 @@ func (s *PoolServer) RemoveCartCookie(w http.ResponseWriter, r *http.Request, ca
|
||||
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 {
|
||||
return func(cartId cart.CartId, w http.ResponseWriter, r *http.Request) error {
|
||||
if ownerHost, ok := s.OwnerHost(uint64(cartId)); ok {
|
||||
@@ -504,7 +281,7 @@ func (s *PoolServer) ProxyHandler(fn func(w http.ResponseWriter, r *http.Request
|
||||
span.SetAttributes(hostAttr)
|
||||
logger.InfoContext(ctx, "cart proxyed", "result", ownerHost.Name())
|
||||
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()
|
||||
if err == nil && handled {
|
||||
@@ -520,13 +297,10 @@ func (s *PoolServer) ProxyHandler(fn func(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
var (
|
||||
tracer = otel.Tracer(name)
|
||||
|
||||
tracer = otel.Tracer(name)
|
||||
meter = otel.Meter(name)
|
||||
logger = otelslog.NewLogger(name)
|
||||
proxyCalls metric.Int64Counter
|
||||
|
||||
// rollCnt metric.Int64Counter
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -550,10 +324,6 @@ func (s *PoolServer) AddVoucherHandler(w http.ResponseWriter, r *http.Request, c
|
||||
v := voucher.Service{}
|
||||
msg, err := v.GetVoucher(data.VoucherCode)
|
||||
if err != nil {
|
||||
s.ApplyLocal(r.Context(), cartId, &messages.PreConditionFailed{
|
||||
Operation: "AddVoucher",
|
||||
Error: err.Error(),
|
||||
})
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return err
|
||||
@@ -568,15 +338,31 @@ func (s *PoolServer) AddVoucherHandler(w http.ResponseWriter, r *http.Request, c
|
||||
return 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 (s *PoolServer) SubscriptionDetailsHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
|
||||
data := &messages.UpsertSubscriptionDetails{}
|
||||
data := &SubscriptionDetailsRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(data)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return err
|
||||
}
|
||||
reply, err := s.ApplyLocal(r.Context(), cartId, data)
|
||||
reply, err := s.ApplyLocal(r.Context(), cartId, data.ToMessage())
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
@@ -586,26 +372,6 @@ func (s *PoolServer) SubscriptionDetailsHandler(w http.ResponseWriter, r *http.R
|
||||
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 {
|
||||
|
||||
idStr := r.PathValue("voucherId")
|
||||
@@ -625,6 +391,90 @@ func (s *PoolServer) RemoveVoucherHandler(w http.ResponseWriter, r *http.Request
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PoolServer) SetUserIdHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
|
||||
setUserId := messages.SetUserId{}
|
||||
err := json.NewDecoder(r.Body).Decode(&setUserId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply, err := s.ApplyLocal(r.Context(), cartId, &setUserId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.WriteResult(w, reply)
|
||||
}
|
||||
|
||||
func (s *PoolServer) LineItemMarkingHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
|
||||
itemIdStr := r.PathValue("itemId")
|
||||
itemId, err := strconv.ParseInt(itemIdStr, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lineItemMarking := messages.LineItemMarking{Id: uint32(itemId)}
|
||||
err = json.NewDecoder(r.Body).Decode(&lineItemMarking)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply, err := s.ApplyLocal(r.Context(), cartId, &lineItemMarking)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.WriteResult(w, reply)
|
||||
}
|
||||
|
||||
func (s *PoolServer) RemoveLineItemMarkingHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
|
||||
itemIdStr := r.PathValue("itemId")
|
||||
itemId, err := strconv.ParseInt(itemIdStr, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
removeLineItemMarking := messages.RemoveLineItemMarking{Id: uint32(itemId)}
|
||||
reply, err := s.ApplyLocal(r.Context(), cartId, &removeLineItemMarking)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.WriteResult(w, reply)
|
||||
}
|
||||
|
||||
func (s *PoolServer) InternalApplyMutationHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
|
||||
if r.Method != http.MethodPost {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
}
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mutation := &messages.Mutation{}
|
||||
err = proto.Unmarshal(data, mutation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reply, err := s.ApplyLocal(r.Context(), cartId, mutation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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) {
|
||||
|
||||
// mux.HandleFunc("OPTIONS /cart", func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -656,27 +506,25 @@ func (s *PoolServer) Serve(mux *http.ServeMux) {
|
||||
handleFunc("DELETE /cart/{itemId}", CookieCartIdHandler(s.ProxyHandler(s.DeleteItemHandler)))
|
||||
handleFunc("PUT /cart", CookieCartIdHandler(s.ProxyHandler(s.QuantityChangeHandler)))
|
||||
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/subscription-details", CookieCartIdHandler(s.ProxyHandler(s.SubscriptionDetailsHandler)))
|
||||
handleFunc("DELETE /cart/voucher/{voucherId}", CookieCartIdHandler(s.ProxyHandler(s.RemoveVoucherHandler)))
|
||||
handleFunc("PUT /cart/user", CookieCartIdHandler(s.ProxyHandler(s.SetUserIdHandler)))
|
||||
|
||||
handleFunc("PUT /cart/item/{itemId}/marking", CookieCartIdHandler(s.ProxyHandler(s.LineItemMarkingHandler)))
|
||||
handleFunc("DELETE /cart/item/{itemId}/marking", CookieCartIdHandler(s.ProxyHandler(s.RemoveLineItemMarkingHandler)))
|
||||
|
||||
//mux.HandleFunc("GET /cart/checkout", CookieCartIdHandler(s.ProxyHandler(s.HandleCheckout)))
|
||||
//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}/add/{sku}", CartIdHandler(s.ProxyHandler(s.AddSkuToCartHandler)))
|
||||
|
||||
handleFunc("POST /cart/byid/{id}", CartIdHandler(s.ProxyHandler(s.AddSkuRequestHandler)))
|
||||
handleFunc("DELETE /cart/byid/{id}/{itemId}", CartIdHandler(s.ProxyHandler(s.DeleteItemHandler)))
|
||||
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("DELETE /cart/byid/{id}/voucher/{voucherId}", CookieCartIdHandler(s.ProxyHandler(s.RemoveVoucherHandler)))
|
||||
//mux.HandleFunc("GET /cart/byid/{id}/checkout", CartIdHandler(s.ProxyHandler(s.HandleCheckout)))
|
||||
//mux.HandleFunc("GET /cart/byid/{id}/confirmation", CartIdHandler(s.ProxyHandler(s.HandleConfirmation)))
|
||||
|
||||
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("DELETE /cart/byid/{id}/item/{itemId}/marking", CartIdHandler(s.ProxyHandler(s.RemoveLineItemMarkingHandler)))
|
||||
}
|
||||
|
||||
+14
-12
@@ -5,11 +5,10 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.tornberg.me/go-cart-actor/pkg/cart"
|
||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||
|
||||
"github.com/matst80/slask-finder/pkg/index"
|
||||
)
|
||||
|
||||
@@ -61,27 +60,27 @@ func ToItemAddMessage(item *index.DataItem, storeId *string, qty int, country st
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stk := item.GetStock()
|
||||
stock := cart.StockStatus(0)
|
||||
centralStockValue, ok := item.GetStringFieldValue(3)
|
||||
|
||||
if storeId == nil {
|
||||
centralStock, ok := stk[country]
|
||||
if ok {
|
||||
if !item.Buyable {
|
||||
return nil, fmt.Errorf("item not available")
|
||||
}
|
||||
pureNumber := strings.Replace(centralStockValue, "+", "", -1)
|
||||
if centralStock, err := strconv.ParseInt(pureNumber, 10, 64); err == nil {
|
||||
stock = cart.StockStatus(centralStock)
|
||||
}
|
||||
if stock == cart.StockStatus(0) && item.SaleStatus == "TBD" {
|
||||
|
||||
if centralStock == 0 && item.SaleStatus == "TBD" {
|
||||
return nil, fmt.Errorf("no items available")
|
||||
}
|
||||
stock = cart.StockStatus(centralStock)
|
||||
}
|
||||
|
||||
} else {
|
||||
if !item.BuyableInStore {
|
||||
return nil, fmt.Errorf("item not available in store")
|
||||
}
|
||||
storeStock, ok := item.Stock.GetStock()[*storeId]
|
||||
storeStock, ok := stk[*storeId]
|
||||
if ok {
|
||||
stock = cart.StockStatus(storeStock)
|
||||
}
|
||||
@@ -104,6 +103,8 @@ func ToItemAddMessage(item *index.DataItem, storeId *string, qty int, country st
|
||||
category4, _ := item.GetStringFieldValue(13) //Fields[13].(string)
|
||||
category5, _ := item.GetStringFieldValue(14) //.Fields[14].(string)
|
||||
|
||||
cgm, _ := item.GetStringFieldValue(35) // Customer Group Membership
|
||||
|
||||
return &messages.AddItem{
|
||||
ItemId: uint32(item.Id),
|
||||
Quantity: int32(qty),
|
||||
@@ -128,6 +129,7 @@ func ToItemAddMessage(item *index.DataItem, storeId *string, qty int, country st
|
||||
Outlet: outlet,
|
||||
StoreId: storeId,
|
||||
SaleStatus: item.SaleStatus,
|
||||
Cgm: cgm,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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.SetBasicAuth(k.UserName, k.Password)
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
res, err := k.client.Do(req)
|
||||
if 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.SetBasicAuth(k.UserName, k.Password)
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
res, err := k.client.Do(req)
|
||||
if nil != err {
|
||||
return nil, err
|
||||
}
|
||||
@@ -119,7 +119,7 @@ func (k *KlarnaClient) AbortOrder(ctx context.Context, orderId string) error {
|
||||
req = req.WithContext(spanCtx)
|
||||
req.SetBasicAuth(k.UserName, k.Password)
|
||||
|
||||
_, err = http.DefaultClient.Do(req)
|
||||
_, err = k.client.Do(req)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -137,6 +137,6 @@ func (k *KlarnaClient) AcknowledgeOrder(ctx context.Context, orderId string) err
|
||||
req.SetBasicAuth(k.UserName, k.Password)
|
||||
req.Header.Add("Klarna-Idempotency-Key", id.String())
|
||||
|
||||
_, err = http.DefaultClient.Do(req)
|
||||
_, err = k.client.Do(req)
|
||||
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"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"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/messaging"
|
||||
"github.com/redis/go-redis/v9"
|
||||
@@ -19,7 +18,8 @@ import (
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
service *inventory.RedisInventoryService
|
||||
inventoryService *inventory.RedisInventoryService
|
||||
reservationService *inventory.RedisCartReservationService
|
||||
}
|
||||
|
||||
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) {
|
||||
// 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 {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -54,6 +48,20 @@ func (srv *Server) getInventoryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
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 redisAddress = "10.10.3.18:6379"
|
||||
var redisPassword = "slaskredis"
|
||||
@@ -87,12 +95,19 @@ func main() {
|
||||
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
|
||||
http.HandleFunc("/livez", server.livezHandler)
|
||||
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{
|
||||
MainStockLocationID: inventory.LocationID(country),
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
"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/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
+212
-19
@@ -38,13 +38,11 @@ spec:
|
||||
volumes:
|
||||
- name: data
|
||||
nfs:
|
||||
path: /i-data/7a8af061/nfs/cart-actor
|
||||
path: /i-data/7a8af061/nfs/
|
||||
server: 10.10.1.10
|
||||
imagePullSecrets:
|
||||
- name: regcred
|
||||
serviceAccountName: default
|
||||
containers:
|
||||
- image: registry.knatofs.se/go-cart-actor-amd64:latest
|
||||
- image: registry.k6n.net/go-cart-actor-amd64:latest
|
||||
name: cart-actor-amd64
|
||||
imagePullPolicy: Always
|
||||
command: ["/go-cart-backoffice"]
|
||||
@@ -78,8 +76,26 @@ spec:
|
||||
memory: "70Mi"
|
||||
cpu: "1200m"
|
||||
env:
|
||||
- name: DATA_DIR
|
||||
value: "/data/cart-actor"
|
||||
- name: CHECKOUT_DATA_DIR
|
||||
value: "/data/checkout-actor"
|
||||
- name: TZ
|
||||
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
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -129,11 +145,9 @@ spec:
|
||||
nfs:
|
||||
path: /i-data/7a8af061/nfs/cart-actor
|
||||
server: 10.10.1.10
|
||||
imagePullSecrets:
|
||||
- name: regcred
|
||||
serviceAccountName: default
|
||||
containers:
|
||||
- image: registry.knatofs.se/go-cart-actor-amd64:latest
|
||||
- image: registry.k6n.net/go-cart-actor-amd64:latest
|
||||
name: cart-actor-amd64
|
||||
imagePullPolicy: Always
|
||||
lifecycle:
|
||||
@@ -170,6 +184,10 @@ spec:
|
||||
memory: "70Mi"
|
||||
cpu: "1200m"
|
||||
env:
|
||||
- name: DATA_DIR
|
||||
value: "/data/cart-actor"
|
||||
- name: CHECKOUT_DATA_DIR
|
||||
value: "/data/checkout-actor"
|
||||
- name: TZ
|
||||
value: "Europe/Stockholm"
|
||||
- name: KLARNA_API_USERNAME
|
||||
@@ -185,6 +203,16 @@ spec:
|
||||
value: "service.name=cart,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_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -241,11 +269,9 @@ spec:
|
||||
nfs:
|
||||
path: /i-data/7a8af061/nfs/cart-actor
|
||||
server: 10.10.1.10
|
||||
imagePullSecrets:
|
||||
- name: regcred
|
||||
serviceAccountName: default
|
||||
containers:
|
||||
- image: registry.knatofs.se/go-cart-actor:latest
|
||||
- image: registry.k6n.net/go-cart-actor:latest
|
||||
name: cart-actor-arm64
|
||||
imagePullPolicy: Always
|
||||
lifecycle:
|
||||
@@ -264,14 +290,14 @@ spec:
|
||||
path: /livez
|
||||
port: web
|
||||
failureThreshold: 1
|
||||
periodSeconds: 30
|
||||
periodSeconds: 15
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /readyz
|
||||
port: web
|
||||
failureThreshold: 2
|
||||
initialDelaySeconds: 2
|
||||
periodSeconds: 30
|
||||
periodSeconds: 15
|
||||
volumeMounts:
|
||||
- mountPath: "/data"
|
||||
name: data
|
||||
@@ -292,6 +318,16 @@ spec:
|
||||
value: "service.name=cart,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:
|
||||
@@ -329,6 +365,140 @@ spec:
|
||||
ports:
|
||||
- name: web
|
||||
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
|
||||
apiVersion: v1
|
||||
@@ -356,10 +526,10 @@ spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- cart.tornberg.me
|
||||
- cart.k6n.net
|
||||
secretName: cart-actor-tls-secret
|
||||
rules:
|
||||
- host: cart.tornberg.me
|
||||
- host: cart.k6n.net
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
@@ -369,6 +539,20 @@ spec:
|
||||
name: cart-actor
|
||||
port:
|
||||
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
|
||||
kind: Ingress
|
||||
@@ -380,10 +564,10 @@ spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- slask-cart.tornberg.me
|
||||
- slask-cart.k6n.net
|
||||
secretName: cart-backoffice-actor-tls-secret
|
||||
rules:
|
||||
- host: slask-cart.tornberg.me
|
||||
- host: slask-cart.k6n.net
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
@@ -422,11 +606,9 @@ spec:
|
||||
operator: NotIn
|
||||
values:
|
||||
- arm64
|
||||
imagePullSecrets:
|
||||
- name: regcred
|
||||
serviceAccountName: default
|
||||
containers:
|
||||
- image: registry.knatofs.se/go-cart-actor-amd64:latest
|
||||
- image: registry.k6n.net/go-cart-actor-amd64:latest
|
||||
name: cart-inventory-amd64
|
||||
imagePullPolicy: Always
|
||||
command: ["/go-cart-inventory"]
|
||||
@@ -466,3 +648,14 @@ spec:
|
||||
value: "redis.home:6379"
|
||||
- name: REDIS_PASSWORD
|
||||
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 (
|
||||
git.tornberg.me/mats/go-redis-inventory v0.0.0-20251113201741-8bf0efac50ee
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
github.com/adyen/adyen-go-api-library/v21 v21.1.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/matst80/slask-finder v0.0.0-20251113121253-1d0cc78309a0
|
||||
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/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/instrumentation/net/http/otelhttp v0.63.0
|
||||
go.opentelemetry.io/otel v1.38.0
|
||||
@@ -22,7 +22,7 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/log v0.14.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0
|
||||
go.opentelemetry.io/otel/trace v1.38.0
|
||||
google.golang.org/grpc v1.76.0
|
||||
google.golang.org/grpc v1.77.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
k8s.io/api v0.34.2
|
||||
k8s.io/apimachinery v0.34.2
|
||||
@@ -32,37 +32,37 @@ require (
|
||||
require (
|
||||
github.com/RoaringBitmap/roaring/v2 v2.14.4 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.3 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dprotaso/go-yit v0.0.0-20250909171706-0a81c39169bc // indirect
|
||||
github.com/dprotaso/go-yit v0.0.0-20251117151522-da16f3077589 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/getkin/kin-openapi v0.133.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.1 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.3 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.3 // indirect
|
||||
github.com/go-openapi/swag v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/cmdutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/loading v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/mangling v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/netutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.1 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/go-openapi/swag v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/loading v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/mangling v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/netutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4 // 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/gorilla/schema v1.4.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.9.1 // indirect
|
||||
@@ -77,20 +77,20 @@ require (
|
||||
github.com/perimeterx/marshmallow v1.1.5 // 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/common v0.67.2 // indirect
|
||||
github.com/prometheus/common v0.67.4 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/speakeasy-api/jsonpath v0.6.2 // indirect
|
||||
github.com/speakeasy-api/openapi-overlay v0.10.3 // indirect
|
||||
github.com/spf13/cobra v1.7.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
|
||||
github.com/woodsbury/decimal128 v1.4.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/oauth2 v0.33.0 // indirect
|
||||
@@ -100,18 +100,18 @@ require (
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/time v0.14.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/rpc 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-20251124214823-79d6a2a48846 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.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
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // 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
|
||||
)
|
||||
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
git.tornberg.me/mats/go-redis-inventory v0.0.0-20251110193851-19d7ad0de6e5 h1:54ZKuqppO6reMmnWOYJaFMlPJK947xnPrv3zDbSuknQ=
|
||||
git.tornberg.me/mats/go-redis-inventory v0.0.0-20251110193851-19d7ad0de6e5/go.mod h1:jrDU55O7sdN2RJr99upmig/FAla/mW1Cdju7834TXug=
|
||||
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.13.0 h1:38BxJ6lGPcBLykIRCyYtViB/By3+a/iS9znKsiBbhNc=
|
||||
github.com/RoaringBitmap/roaring/v2 v2.13.0/go.mod h1:Mpi+oQ+3oCU7g1aF75Ib/XYCTqjTGpHI0f8djSZVY3I=
|
||||
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/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/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bits-and-blooms/bitset v1.24.1 h1:hqnfFbjjk3pxGa5E9Ho3hjoU7odtUuNmJ9Ao+Bo8s1c=
|
||||
github.com/bits-and-blooms/bitset v1.24.1/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.24.3 h1:Bte86SlO3lwPQqww+7BE9ZuUCKIjfqnG5jtEyqA9y9Y=
|
||||
github.com/bits-and-blooms/bitset v1.24.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
|
||||
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
@@ -20,10 +16,6 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -31,21 +23,17 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
|
||||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w=
|
||||
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q=
|
||||
github.com/dprotaso/go-yit v0.0.0-20250909171706-0a81c39169bc h1:YxqE1wh+qGVXQFinuRq5lT77h6baDtBnAVh61LlXp1o=
|
||||
github.com/dprotaso/go-yit v0.0.0-20250909171706-0a81c39169bc/go.mod h1:5NQLChvz4dnEIQ8WcHIFbZ1bp0GEUZHiBH+EpTZ4lBc=
|
||||
github.com/dprotaso/go-yit v0.0.0-20251117151522-da16f3077589 h1:VJ/jVUWr+r4MQA7U/cscbbXRuwh1PfPCUUItYAjlKN4=
|
||||
github.com/dprotaso/go-yit v0.0.0-20251117151522-da16f3077589/go.mod h1:IeI20psFPeg2n1jxwbkYCmkpYsXsJqB7qmoqCIlX80s=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk=
|
||||
github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
|
||||
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
|
||||
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -53,40 +41,40 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
|
||||
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
|
||||
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
|
||||
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
|
||||
github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8=
|
||||
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/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
|
||||
github.com/go-openapi/swag v0.25.1 h1:6uwVsx+/OuvFVPqfQmOOPsqTcm5/GkBhNwLqIR916n8=
|
||||
github.com/go-openapi/swag v0.25.1/go.mod h1:bzONdGlT0fkStgGPd3bhZf1MnuPkf2YAys6h+jZipOo=
|
||||
github.com/go-openapi/swag/cmdutils v0.25.1 h1:nDke3nAFDArAa631aitksFGj2omusks88GF1VwdYqPY=
|
||||
github.com/go-openapi/swag/cmdutils v0.25.1/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
|
||||
github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0=
|
||||
github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs=
|
||||
github.com/go-openapi/swag/fileutils v0.25.1 h1:rSRXapjQequt7kqalKXdcpIegIShhTPXx7yw0kek2uU=
|
||||
github.com/go-openapi/swag/fileutils v0.25.1/go.mod h1:+NXtt5xNZZqmpIpjqcujqojGFek9/w55b3ecmOdtg8M=
|
||||
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
|
||||
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg=
|
||||
github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw=
|
||||
github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc=
|
||||
github.com/go-openapi/swag/mangling v0.25.1 h1:XzILnLzhZPZNtmxKaz/2xIGPQsBsvmCjrJOWGNz/ync=
|
||||
github.com/go-openapi/swag/mangling v0.25.1/go.mod h1:CdiMQ6pnfAgyQGSOIYnZkXvqhnnwOn997uXZMAd/7mQ=
|
||||
github.com/go-openapi/swag/netutils v0.25.1 h1:2wFLYahe40tDUHfKT1GRC4rfa5T1B4GWZ+msEFA4Fl4=
|
||||
github.com/go-openapi/swag/netutils v0.25.1/go.mod h1:CAkkvqnUJX8NV96tNhEQvKz8SQo2KF0f7LleiJwIeRE=
|
||||
github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw=
|
||||
github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg=
|
||||
github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA=
|
||||
github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU=
|
||||
github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ=
|
||||
github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4=
|
||||
github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=
|
||||
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
|
||||
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
|
||||
github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y=
|
||||
github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk=
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
|
||||
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
|
||||
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
|
||||
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
|
||||
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
|
||||
github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48=
|
||||
github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg=
|
||||
github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0=
|
||||
github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg=
|
||||
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
|
||||
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
|
||||
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
|
||||
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
|
||||
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/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/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
|
||||
@@ -94,40 +82,24 @@ github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncV
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
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.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
|
||||
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/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
|
||||
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
||||
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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
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/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
@@ -145,14 +117,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
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/matst80/slask-finder v0.0.0-20251023104024-f788e5a51d68 h1:nDSim5IRFrLG3okBty3nes50ZuxKMEY5rwj2FuUnTgc=
|
||||
github.com/matst80/slask-finder v0.0.0-20251023104024-f788e5a51d68/go.mod h1:ZlbURsmhMX98D8z/du5Cez8fZU/sfkA98ruczIsB9PY=
|
||||
github.com/matst80/slask-finder v0.0.0-20251113121253-1d0cc78309a0 h1:ZBbVCFAvccFLOck4e64BtNXdexouVHfQqQzlZaEIY40=
|
||||
github.com/matst80/slask-finder v0.0.0-20251113121253-1d0cc78309a0/go.mod h1:554hsg38gorr5GH/BLGs4pPqVpmrHTNWq5qoferDeDA=
|
||||
github.com/matst80/go-redis-inventory v0.0.0-20251126173508-51b30de2d86e h1:Z7A73W6jsxFuFKWvB1efQmTjs0s7+x2B7IBM2ukkI6Y=
|
||||
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-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -165,12 +135,8 @@ github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
|
||||
github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 h1:iJvF8SdB/3/+eGOXEpsWkD8FQAHj6mqkb6Fnsoc8MFU=
|
||||
github.com/oapi-codegen/oapi-codegen/v2 v2.5.0/go.mod h1:fwlMxUEMuQK5ih9aymrxKPQqNm2n8bdLk1ppjH+lr9w=
|
||||
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
|
||||
github.com/oapi-codegen/oapi-codegen/v2 v2.5.1 h1:5vHNY1uuPBRBWqB2Dp0G7YB03phxLQZupZTIZaeorjc=
|
||||
github.com/oapi-codegen/oapi-codegen/v2 v2.5.1/go.mod h1:ro0npU1BWkcGpCgGD9QwPp44l5OIZ94tB3eabnT7DjQ=
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
|
||||
@@ -179,21 +145,13 @@ github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletI
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -203,34 +161,22 @@ 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_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI=
|
||||
github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q=
|
||||
github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8=
|
||||
github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko=
|
||||
github.com/prometheus/procfs v0.18.0 h1:2QTA9cKdznfYJz7EDaa7IiJobHuV7E1WzeBwcrhk0ao=
|
||||
github.com/prometheus/procfs v0.18.0/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
||||
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/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
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/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4=
|
||||
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/redis/go-redis/v9 v9.17.0 h1:K6E+ZlYN95KSMmZeEQPbU/c++wfmEvfFB17yEAq/VhM=
|
||||
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/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8=
|
||||
github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw=
|
||||
github.com/speakeasy-api/jsonpath v0.6.2 h1:Mys71yd6u8kuowNCR0gCVPlVAHCmKtoGXYoAtcEbqXQ=
|
||||
github.com/speakeasy-api/jsonpath v0.6.2/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw=
|
||||
github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU=
|
||||
github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg=
|
||||
github.com/speakeasy-api/openapi-overlay v0.10.3 h1:70een4vwHyslIp796vM+ox6VISClhtXsCjrQNhxwvWs=
|
||||
github.com/speakeasy-api/openapi-overlay v0.10.3/go.mod h1:RJjV0jbUHqXLS0/Mxv5XE7LAnJHqHw+01RDdpoGqiyY=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -245,10 +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/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/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc=
|
||||
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/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
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/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/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0 h1:bwnLpizECbPr1RrQ27waeY2SPIPeccCx/xLuoYADZ9s=
|
||||
@@ -279,8 +229,6 @@ go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -289,71 +237,40 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
@@ -361,10 +278,7 @@ golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -373,24 +287,12 @@ 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=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846 h1:ZdyUkS9po3H7G0tuh955QVyyotWvOD4W0aEapeGeUYk=
|
||||
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-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk=
|
||||
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/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -407,36 +309,29 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
|
||||
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
|
||||
k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY=
|
||||
k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw=
|
||||
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
|
||||
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4=
|
||||
k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
|
||||
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
|
||||
k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M=
|
||||
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/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ=
|
||||
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/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/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
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/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
||||
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 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E=
|
||||
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/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
|
||||
+7
-7
@@ -18,10 +18,10 @@ This directory contains a k6 script (`cart_load_test.js`) to stress and observe
|
||||
|
||||
The script exercises (per iteration):
|
||||
|
||||
1. `GET /cart/` – ensure / fetch cart state (creates cart if missing; sets `cartid` & `cartowner` cookies)
|
||||
2. `POST /cart/` – add item mutation (random SKU & quantity)
|
||||
3. `GET /cart/` – fetch after mutations
|
||||
4. `GET /cart/checkout` – occasionally (~2% of iterations) to simulate checkout start
|
||||
1. `GET /cart` – ensure / fetch cart state (creates cart if missing; sets `cartid` & `cartowner` cookies)
|
||||
2. `POST /cart` – add item mutation (random SKU & quantity)
|
||||
3. `GET /cart` – fetch after mutations
|
||||
4. `GET /checkout` – occasionally (~2% of iterations) to simulate checkout start
|
||||
|
||||
You can extend it easily to hit deliveries, quantity changes, or removal endpoints.
|
||||
|
||||
@@ -40,9 +40,9 @@ Example run:
|
||||
|
||||
```bash
|
||||
k6 run \
|
||||
-e BASE_URL=https://cart.prod.example.com/cart \
|
||||
-e BASE_URL=https://cart.k6n.net/cart \
|
||||
-e VUS=40 \
|
||||
-e DURATION=10m \
|
||||
-e DURATION=1m \
|
||||
-e RAMP_TARGET=120 \
|
||||
k6/cart_load_test.js
|
||||
```
|
||||
@@ -171,4 +171,4 @@ Feel free to request:
|
||||
- WebSocket / long poll integration (if added later)
|
||||
- Synthetic error injection harness
|
||||
|
||||
Happy load testing!
|
||||
Happy load testing!
|
||||
|
||||
+166
-170
@@ -4,43 +4,39 @@ import { Counter, Trend } from "k6/metrics";
|
||||
|
||||
// ---------------- Configuration ----------------
|
||||
export const options = {
|
||||
// Adjust vus/duration for your environment
|
||||
scenarios: {
|
||||
steady_mutations: {
|
||||
executor: "constant-vus",
|
||||
vus: __ENV.VUS ? parseInt(__ENV.VUS, 10) : 20,
|
||||
duration: __ENV.DURATION || "5m",
|
||||
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",
|
||||
},
|
||||
// Adjust vus/duration for your environment
|
||||
scenarios: {
|
||||
steady_mutations: {
|
||||
executor: "constant-vus",
|
||||
vus: __ENV.VUS ? parseInt(__ENV.VUS, 10) : 20,
|
||||
duration: __ENV.DURATION || "5m",
|
||||
gracefulStop: "30s",
|
||||
},
|
||||
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"],
|
||||
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",
|
||||
},
|
||||
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 ----------------
|
||||
@@ -52,197 +48,197 @@ const checkoutCounter = new Counter("cart_checkout_calls");
|
||||
|
||||
// ---------------- SKUs ----------------
|
||||
const SKUS = [
|
||||
"778290",
|
||||
"778345",
|
||||
"778317",
|
||||
"778277",
|
||||
"778267",
|
||||
"778376",
|
||||
"778244",
|
||||
"778384",
|
||||
"778365",
|
||||
"778377",
|
||||
"778255",
|
||||
"778286",
|
||||
"778246",
|
||||
"778270",
|
||||
"778266",
|
||||
"778285",
|
||||
"778329",
|
||||
"778425",
|
||||
"778407",
|
||||
"778418",
|
||||
"778430",
|
||||
"778469",
|
||||
"778358",
|
||||
"778351",
|
||||
"778319",
|
||||
"778307",
|
||||
"778278",
|
||||
"778251",
|
||||
"778253",
|
||||
"778261",
|
||||
"778263",
|
||||
"778273",
|
||||
"778281",
|
||||
"778294",
|
||||
"778297",
|
||||
"778302",
|
||||
"778290",
|
||||
"778345",
|
||||
"778317",
|
||||
"778277",
|
||||
"778267",
|
||||
"778376",
|
||||
"778244",
|
||||
"778384",
|
||||
"778365",
|
||||
"778377",
|
||||
"778255",
|
||||
"778286",
|
||||
"778246",
|
||||
"778270",
|
||||
"778266",
|
||||
"778285",
|
||||
"778329",
|
||||
"778425",
|
||||
"778407",
|
||||
"778418",
|
||||
"778430",
|
||||
"778469",
|
||||
"778358",
|
||||
"778351",
|
||||
"778319",
|
||||
"778307",
|
||||
"778278",
|
||||
"778251",
|
||||
"778253",
|
||||
"778261",
|
||||
"778263",
|
||||
"778273",
|
||||
"778281",
|
||||
"778294",
|
||||
"778297",
|
||||
"778302",
|
||||
];
|
||||
|
||||
// ---------------- Helpers ----------------
|
||||
function randomSku() {
|
||||
return SKUS[Math.floor(Math.random() * SKUS.length)];
|
||||
return SKUS[Math.floor(Math.random() * SKUS.length)];
|
||||
}
|
||||
function randomQty() {
|
||||
return 1 + Math.floor(Math.random() * 3); // 1..3
|
||||
return 1 + Math.floor(Math.random() * 3); // 1..3
|
||||
}
|
||||
function baseUrl() {
|
||||
const u = __ENV.BASE_URL || "http://localhost:8080/cart";
|
||||
// Allow user to pass either root host or full /cart path
|
||||
return u.endsWith("/cart") ? u : u.replace(/\/+$/, "") + "/cart";
|
||||
const u = __ENV.BASE_URL || "http://localhost:8080/cart";
|
||||
// Allow user to pass either root host or full /cart path
|
||||
return u.endsWith("/cart") ? u : u.replace(/\/+$/, "") + "/cart";
|
||||
}
|
||||
function extractCookie(res, name) {
|
||||
const cookies = res.cookies[name];
|
||||
if (!cookies || cookies.length === 0) return null;
|
||||
return cookies[0].value;
|
||||
const cookies = res.cookies[name];
|
||||
if (!cookies || cookies.length === 0) return null;
|
||||
return cookies[0].value;
|
||||
}
|
||||
function withCookies(headers, cookieJar) {
|
||||
if (!cookieJar || Object.keys(cookieJar).length === 0) return headers;
|
||||
const cookieStr = Object.entries(cookieJar)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join("; ");
|
||||
return { ...headers, Cookie: cookieStr };
|
||||
if (!cookieJar || Object.keys(cookieJar).length === 0) return headers;
|
||||
const cookieStr = Object.entries(cookieJar)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join("; ");
|
||||
return { ...headers, Cookie: cookieStr };
|
||||
}
|
||||
|
||||
// Maintain cart + owner cookies per VU
|
||||
let cartState = {
|
||||
cartid: null,
|
||||
cartowner: null,
|
||||
cartid: null,
|
||||
cartowner: null,
|
||||
};
|
||||
|
||||
// Refresh cookies from response
|
||||
function updateCookies(res) {
|
||||
const cid = extractCookie(res, "cartid");
|
||||
if (cid) cartState.cartid = cid;
|
||||
const owner = extractCookie(res, "cartowner");
|
||||
if (owner) cartState.cartowner = owner;
|
||||
const cid = extractCookie(res, "cartid");
|
||||
if (cid) cartState.cartid = cid;
|
||||
const owner = extractCookie(res, "cartowner");
|
||||
if (owner) cartState.cartowner = owner;
|
||||
}
|
||||
|
||||
// Build headers
|
||||
function headers() {
|
||||
const h = { "Content-Type": "application/json" };
|
||||
const jar = {};
|
||||
if (cartState.cartid) jar["cartid"] = cartState.cartid;
|
||||
if (cartState.cartowner) jar["cartowner"] = cartState.cartowner;
|
||||
return withCookies(h, jar);
|
||||
const h = { "Content-Type": "application/json" };
|
||||
const jar = {};
|
||||
if (cartState.cartid) jar["cartid"] = cartState.cartid;
|
||||
if (cartState.cartowner) jar["cartowner"] = cartState.cartowner;
|
||||
return withCookies(h, jar);
|
||||
}
|
||||
|
||||
// Ensure cart exists (GET /)
|
||||
function ensureCart() {
|
||||
if (cartState.cartid) return;
|
||||
const res = http.get(baseUrl() + "/", { headers: headers() });
|
||||
updateCookies(res);
|
||||
check(res, {
|
||||
"ensure cart status 200": (r) => r.status === 200,
|
||||
"ensure cart has id": () => !!cartState.cartid,
|
||||
});
|
||||
if (cartState.cartid) return;
|
||||
const res = http.get(baseUrl(), { headers: headers() });
|
||||
updateCookies(res);
|
||||
check(res, {
|
||||
"ensure cart status 200": (r) => r.status === 200,
|
||||
"ensure cart has id": () => !!cartState.cartid,
|
||||
});
|
||||
}
|
||||
|
||||
// Add random item
|
||||
function addRandomItem() {
|
||||
const payload = JSON.stringify({
|
||||
sku: randomSku(),
|
||||
quantity: randomQty(),
|
||||
country: "no",
|
||||
});
|
||||
const start = Date.now();
|
||||
const res = http.post(baseUrl(), payload, { headers: headers() });
|
||||
const dur = Date.now() - start;
|
||||
addItemTrend.add(dur, { op: "add" });
|
||||
if (res.status === 200) {
|
||||
addedItemsCounter.add(1);
|
||||
}
|
||||
updateCookies(res);
|
||||
check(res, {
|
||||
"add item status ok": (r) => r.status === 200,
|
||||
});
|
||||
const payload = JSON.stringify({
|
||||
sku: randomSku(),
|
||||
quantity: randomQty(),
|
||||
country: "no",
|
||||
});
|
||||
const start = Date.now();
|
||||
const res = http.post(baseUrl(), payload, { headers: headers() });
|
||||
const dur = Date.now() - start;
|
||||
addItemTrend.add(dur, { op: "add" });
|
||||
if (res.status === 200) {
|
||||
addedItemsCounter.add(1);
|
||||
}
|
||||
updateCookies(res);
|
||||
check(res, {
|
||||
"add item status ok": (r) => r.status === 200,
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch cart state
|
||||
function fetchCart() {
|
||||
const start = Date.now();
|
||||
const res = http.get(baseUrl() + "/", { headers: headers() });
|
||||
const dur = Date.now() - start;
|
||||
fetchTrend.add(dur, { op: "get" });
|
||||
updateCookies(res);
|
||||
check(res, { "fetch status ok": (r) => r.status === 200 });
|
||||
const start = Date.now();
|
||||
const res = http.get(baseUrl(), { headers: headers() });
|
||||
const dur = Date.now() - start;
|
||||
fetchTrend.add(dur, { op: "get" });
|
||||
updateCookies(res);
|
||||
check(res, { "fetch status ok": (r) => r.status === 200 });
|
||||
}
|
||||
|
||||
// Occasional checkout trigger
|
||||
function maybeCheckout() {
|
||||
if (!cartState.cartid) return;
|
||||
// // Small probability
|
||||
// if (Math.random() < 0.02) {
|
||||
// const start = Date.now();
|
||||
// const res = http.get(baseUrl() + "/checkout", { headers: headers() });
|
||||
// const dur = Date.now() - start;
|
||||
// checkoutTrend.add(dur, { op: "checkout" });
|
||||
// updateCookies(res);
|
||||
// if (res.status === 200) checkoutCounter.add(1);
|
||||
// check(res, { "checkout status ok": (r) => r.status === 200 });
|
||||
// }
|
||||
if (!cartState.cartid) return;
|
||||
// // Small probability
|
||||
// if (Math.random() < 0.02) {
|
||||
// const start = Date.now();
|
||||
// const res = http.get(baseUrl() + "/checkout", { headers: headers() });
|
||||
// const dur = Date.now() - start;
|
||||
// checkoutTrend.add(dur, { op: "checkout" });
|
||||
// updateCookies(res);
|
||||
// if (res.status === 200) checkoutCounter.add(1);
|
||||
// check(res, { "checkout status ok": (r) => r.status === 200 });
|
||||
// }
|
||||
}
|
||||
|
||||
// ---------------- k6 lifecycle ----------------
|
||||
export function setup() {
|
||||
// Provide SKU list length for summary
|
||||
return { skuCount: SKUS.length };
|
||||
// Provide SKU list length for summary
|
||||
return { skuCount: SKUS.length };
|
||||
}
|
||||
|
||||
export default function (data) {
|
||||
group("cart flow", () => {
|
||||
// Create or reuse cart
|
||||
ensureCart();
|
||||
group("cart flow", () => {
|
||||
// Create or reuse cart
|
||||
ensureCart();
|
||||
|
||||
// Random number of item mutations per iteration (1..5)
|
||||
const ops = 1 + Math.floor(Math.random() * 5);
|
||||
for (let i = 0; i < ops; i++) {
|
||||
addRandomItem();
|
||||
}
|
||||
// Random number of item mutations per iteration (1..5)
|
||||
const ops = 1 + Math.floor(Math.random() * 5);
|
||||
for (let i = 0; i < ops; i++) {
|
||||
addRandomItem();
|
||||
}
|
||||
|
||||
// Fetch state
|
||||
fetchCart();
|
||||
// Fetch state
|
||||
fetchCart();
|
||||
|
||||
// Optional checkout attempt
|
||||
maybeCheckout();
|
||||
});
|
||||
// Optional checkout attempt
|
||||
maybeCheckout();
|
||||
});
|
||||
|
||||
// Small think time
|
||||
sleep(Math.random() * 0.5);
|
||||
// Small think time
|
||||
sleep(Math.random() * 0.5);
|
||||
}
|
||||
|
||||
export function teardown(data) {
|
||||
// Optionally we could GET confirmation or clear cart cookie
|
||||
// Not implemented for load purpose.
|
||||
console.log(`Test complete. SKU count: ${data.skuCount}`);
|
||||
// Optionally we could GET confirmation or clear cart cookie
|
||||
// Not implemented for load purpose.
|
||||
console.log(`Test complete. SKU count: ${data.skuCount}`);
|
||||
}
|
||||
|
||||
// ---------------- Summary ----------------
|
||||
export function handleSummary(data) {
|
||||
return {
|
||||
stdout: JSON.stringify(
|
||||
{
|
||||
metrics: {
|
||||
mutations_avg: data.metrics.cart_add_item_duration?.avg,
|
||||
mutations_p95: data.metrics.cart_add_item_duration?.p(95),
|
||||
fetch_p95: data.metrics.cart_fetch_duration?.p(95),
|
||||
checkout_count: data.metrics.cart_checkout_calls?.count,
|
||||
},
|
||||
checks: data.root_checks,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
};
|
||||
return {
|
||||
stdout: JSON.stringify(
|
||||
{
|
||||
metrics: {
|
||||
mutations_avg: data.metrics.cart_add_item_duration?.avg,
|
||||
mutations_p95: data.metrics.cart_add_item_duration?.p(95),
|
||||
fetch_p95: data.metrics.cart_fetch_duration?.p(95),
|
||||
checkout_count: data.metrics.cart_checkout_calls?.count,
|
||||
},
|
||||
checks: data.root_checks,
|
||||
},
|
||||
null,
|
||||
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"
|
||||
"time"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type QueueEvent struct {
|
||||
@@ -27,6 +27,7 @@ type DiskStorage[V any] struct {
|
||||
|
||||
type LogStorage[V any] interface {
|
||||
LoadEvents(ctx context.Context, id uint64, grain Grain[V]) error
|
||||
LoadEventsFunc(ctx context.Context, id uint64, grain Grain[V], condition func(msg proto.Message, index int, timeStamp time.Time) bool) error
|
||||
AppendMutations(id uint64, msg ...proto.Message) error
|
||||
}
|
||||
|
||||
@@ -87,6 +88,27 @@ func (s *DiskStorage[V]) logPath(id uint64) string {
|
||||
return filepath.Join(s.path, fmt.Sprintf("%d.events.log", id))
|
||||
}
|
||||
|
||||
func (s *DiskStorage[V]) LoadEventsFunc(ctx context.Context, id uint64, grain Grain[V], condition func(msg proto.Message, index int, timeStamp time.Time) bool) error {
|
||||
path := s.logPath(id)
|
||||
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
|
||||
// No log -> nothing to replay
|
||||
return nil
|
||||
}
|
||||
|
||||
fh, err := os.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open replay file: %w", err)
|
||||
}
|
||||
defer fh.Close()
|
||||
index := 0
|
||||
return s.Load(fh, func(msg proto.Message, when time.Time) {
|
||||
if condition(msg, index, when) {
|
||||
s.registry.Apply(ctx, grain, msg)
|
||||
}
|
||||
index++
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DiskStorage[V]) LoadEvents(ctx context.Context, id uint64, grain Grain[V]) error {
|
||||
path := s.logPath(id)
|
||||
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
|
||||
@@ -99,7 +121,7 @@ func (s *DiskStorage[V]) LoadEvents(ctx context.Context, id uint64, grain Grain[
|
||||
return fmt.Errorf("open replay file: %w", err)
|
||||
}
|
||||
defer fh.Close()
|
||||
return s.Load(fh, func(msg proto.Message) {
|
||||
return s.Load(fh, func(msg proto.Message, _ time.Time) {
|
||||
s.registry.Apply(ctx, grain, msg)
|
||||
})
|
||||
}
|
||||
|
||||
+11
-8
@@ -2,9 +2,10 @@ package actor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type MutationResult[V any] struct {
|
||||
@@ -14,27 +15,29 @@ type MutationResult[V any] struct {
|
||||
|
||||
type GrainPool[V any] interface {
|
||||
Apply(ctx context.Context, id uint64, mutation ...proto.Message) (*MutationResult[V], error)
|
||||
Get(ctx context.Context, id uint64) (V, error)
|
||||
OwnerHost(id uint64) (Host, bool)
|
||||
Get(ctx context.Context, id uint64) (*V, error)
|
||||
OwnerHost(id uint64) (Host[V], bool)
|
||||
Hostname() string
|
||||
TakeOwnership(id uint64)
|
||||
HandleOwnershipChange(host string, ids []uint64) error
|
||||
HandleRemoteExpiry(host string, ids []uint64) error
|
||||
Negotiate(otherHosts []string)
|
||||
GetLocalIds() []uint64
|
||||
IsHealthy() bool
|
||||
Close()
|
||||
IsKnown(string) bool
|
||||
RemoveHost(host string)
|
||||
AddRemoteHost(host string)
|
||||
IsHealthy() bool
|
||||
IsKnown(string) bool
|
||||
Close()
|
||||
}
|
||||
|
||||
// Host abstracts a remote node capable of proxying cart requests.
|
||||
type Host interface {
|
||||
type Host[V any] interface {
|
||||
AnnounceExpiry(ids []uint64)
|
||||
Negotiate(otherHosts []string) ([]string, error)
|
||||
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
|
||||
Close() error
|
||||
Ping() bool
|
||||
|
||||
@@ -2,18 +2,21 @@ package actor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"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/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/reflection"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
)
|
||||
|
||||
// ControlServer implements the ControlPlane gRPC services.
|
||||
@@ -97,13 +100,37 @@ func (s *ControlServer[V]) AnnounceOwnership(ctx context.Context, req *messages.
|
||||
}, err
|
||||
}
|
||||
|
||||
log.Printf("Ack count: %d", len(req.Ids))
|
||||
// log.Printf("Ack count: %d", len(req.Ids))
|
||||
return &messages.OwnerChangeAck{
|
||||
Accepted: true,
|
||||
Message: "ownership announced",
|
||||
}, 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) {
|
||||
ctx, span := tracer.Start(ctx, "grpc_announce_expiry")
|
||||
defer span.End()
|
||||
@@ -126,7 +153,7 @@ func (s *ControlServer[V]) AnnounceExpiry(ctx context.Context, req *messages.Exp
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
@@ -139,6 +166,47 @@ func (s *ControlServer[V]) Ping(ctx context.Context, _ *messages.Empty) (*messag
|
||||
}, 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)
|
||||
func (s *ControlServer[V]) Negotiate(ctx context.Context, req *messages.NegotiateRequest) (*messages.NegotiateReply, error) {
|
||||
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)
|
||||
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")
|
||||
defer span.End()
|
||||
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"
|
||||
"sync"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type ApplyResult struct {
|
||||
@@ -41,14 +41,16 @@ type MutationRegistry interface {
|
||||
Create(typeName string) (proto.Message, bool)
|
||||
GetTypeName(msg proto.Message) (string, bool)
|
||||
RegisterProcessor(processor ...MutationProcessor)
|
||||
//GetStorageEvent(msg proto.Message) StorageEvent
|
||||
//FromStorageEvent(event StorageEvent) (proto.Message, error)
|
||||
RegisterTrigger(trigger ...TriggerHandler)
|
||||
SetEventChannel(ch chan<- ApplyResult)
|
||||
}
|
||||
|
||||
type ProtoMutationRegistry struct {
|
||||
mutationRegistryMu sync.RWMutex
|
||||
mutationRegistry map[reflect.Type]MutationHandler
|
||||
triggers map[reflect.Type][]TriggerHandler
|
||||
processors []MutationProcessor
|
||||
eventChannel chan<- ApplyResult
|
||||
}
|
||||
|
||||
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 {
|
||||
Handle(state any, msg proto.Message) error
|
||||
Name() string
|
||||
@@ -95,17 +117,26 @@ type MutationHandler interface {
|
||||
type RegisteredMutation[V any, T proto.Message] struct {
|
||||
name string
|
||||
handler func(*V, T) error
|
||||
create func() T
|
||||
create func() proto.Message
|
||||
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().
|
||||
// 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.
|
||||
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()
|
||||
rt := reflect.TypeOf(instance)
|
||||
if rt.Kind() == reflect.Ptr {
|
||||
if rt.Kind() == reflect.Pointer {
|
||||
rt = rt.Elem()
|
||||
}
|
||||
return &RegisteredMutation[V, T]{
|
||||
@@ -136,6 +167,7 @@ func NewMutationRegistry() MutationRegistry {
|
||||
return &ProtoMutationRegistry{
|
||||
mutationRegistry: make(map[reflect.Type]MutationHandler),
|
||||
mutationRegistryMu: sync.RWMutex{},
|
||||
triggers: make(map[reflect.Type][]TriggerHandler),
|
||||
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) {
|
||||
r.mutationRegistryMu.RLock()
|
||||
defer r.mutationRegistryMu.RUnlock()
|
||||
@@ -212,13 +262,13 @@ func (r *ProtoMutationRegistry) Apply(ctx context.Context, grain any, msg ...pro
|
||||
}
|
||||
|
||||
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 {
|
||||
continue
|
||||
return results, fmt.Errorf("nil mutation message")
|
||||
}
|
||||
// Typed nil: interface holds concrete proto message type whose pointer value is nil.
|
||||
rv := reflect.ValueOf(m)
|
||||
if rv.Kind() == reflect.Ptr && rv.IsNil() {
|
||||
if rv.Kind() == reflect.Pointer && rv.IsNil() {
|
||||
continue
|
||||
}
|
||||
rt := indirectType(reflect.TypeOf(m))
|
||||
@@ -235,6 +285,25 @@ func (r *ProtoMutationRegistry) Apply(ctx context.Context, grain any, msg ...pro
|
||||
if err != nil {
|
||||
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})
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -2,38 +2,37 @@ package actor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
cart_messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||
)
|
||||
|
||||
type cartState struct {
|
||||
calls int
|
||||
lastAdded *messages.AddItem
|
||||
lastAdded *cart_messages.AddItem
|
||||
}
|
||||
|
||||
func TestRegisteredMutationBasics(t *testing.T) {
|
||||
reg := NewMutationRegistry().(*ProtoMutationRegistry)
|
||||
|
||||
addItemMutation := NewMutation(
|
||||
func(state *cartState, msg *messages.AddItem) error {
|
||||
func(state *cartState, msg *cart_messages.AddItem) error {
|
||||
state.calls++
|
||||
// copy to avoid external mutation side-effects (not strictly necessary for the test)
|
||||
cp := msg
|
||||
state.lastAdded = cp
|
||||
return nil
|
||||
},
|
||||
func() *messages.AddItem { return &messages.AddItem{} },
|
||||
)
|
||||
|
||||
// Sanity check on mutation metadata
|
||||
if addItemMutation.Name() != "AddItem" {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -47,18 +46,18 @@ func TestRegisteredMutationBasics(t *testing.T) {
|
||||
|
||||
// RegisteredMutationTypes: membership (order not guaranteed)
|
||||
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)
|
||||
}
|
||||
|
||||
// GetTypeName should resolve for a pointer instance
|
||||
name, ok := reg.GetTypeName(&messages.AddItem{})
|
||||
name, ok := reg.GetTypeName(&cart_messages.AddItem{})
|
||||
if !ok || name != "AddItem" {
|
||||
t.Fatalf("GetTypeName returned (%q,%v), expected (AddItem,true)", name, ok)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -67,7 +66,7 @@ func TestRegisteredMutationBasics(t *testing.T) {
|
||||
if !ok {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -78,7 +77,7 @@ func TestRegisteredMutationBasics(t *testing.T) {
|
||||
|
||||
// Apply happy path
|
||||
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 {
|
||||
t.Fatalf("Apply returned error: %v", err)
|
||||
}
|
||||
@@ -90,7 +89,7 @@ func TestRegisteredMutationBasics(t *testing.T) {
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
@@ -100,36 +99,109 @@ func TestRegisteredMutationBasics(t *testing.T) {
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// func TestConcurrentSafeRegistrationLookup(t *testing.T) {
|
||||
// // This test is light-weight; it ensures locks don't deadlock under simple concurrent access.
|
||||
// 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)
|
||||
func TestEventChannel(t *testing.T) {
|
||||
reg := NewMutationRegistry().(*ProtoMutationRegistry)
|
||||
|
||||
// done := make(chan struct{})
|
||||
// const workers = 25
|
||||
// for i := 0; i < workers; i++ {
|
||||
// go func() {
|
||||
// for j := 0; j < 100; j++ {
|
||||
// _, _ = reg.Create("Noop")
|
||||
// _, _ = reg.GetTypeName(&messages.Noop{})
|
||||
// _ = reg.Apply(&cartState{}, &messages.Noop{})
|
||||
// }
|
||||
// done <- struct{}{}
|
||||
// }()
|
||||
// }
|
||||
addItemMutation := NewMutation(
|
||||
func(state *cartState, msg *cart_messages.AddItem) error {
|
||||
state.calls++
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
// for i := 0; i < workers; i++ {
|
||||
// <-done
|
||||
// }
|
||||
// }
|
||||
reg.RegisterMutations(addItemMutation)
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package actor
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"slices"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ReceiverFunc[V any] func(event V)
|
||||
|
||||
type PubSub[V any] struct {
|
||||
subscribers []*ReceiverFunc[V]
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewPubSub creates a new PubSub instance.
|
||||
func NewPubSub[V any]() *PubSub[V] {
|
||||
return &PubSub[V]{
|
||||
subscribers: make([]*ReceiverFunc[V], 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe adds a grain ID to the subscribers of a topic.
|
||||
func (p *PubSub[V]) Subscribe(receiver ReceiverFunc[V]) {
|
||||
if receiver == nil {
|
||||
return
|
||||
}
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
// log.Printf("adding subscriber")
|
||||
p.subscribers = append(p.subscribers, &receiver)
|
||||
}
|
||||
|
||||
// Unsubscribe removes a grain ID from the subscribers of a topic.
|
||||
func (p *PubSub[V]) Unsubscribe(receiver ReceiverFunc[V]) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
list := p.subscribers
|
||||
prt := &receiver
|
||||
for i, sub := range list {
|
||||
if sub == nil {
|
||||
continue
|
||||
}
|
||||
if sub == prt {
|
||||
// log.Printf("removing subscriber")
|
||||
p.subscribers = append(list[:i], list[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
p.subscribers = slices.DeleteFunc(p.subscribers, func(fn *ReceiverFunc[V]) bool {
|
||||
return fn == nil
|
||||
})
|
||||
// If list is empty, could delete, but not necessary
|
||||
}
|
||||
|
||||
// GetSubscribers returns a copy of the subscriber IDs for a topic.
|
||||
func (p *PubSub[V]) GetSubscribers() iter.Seq[ReceiverFunc[V]] {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
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.
|
||||
func (p *PubSub[V]) Publish(event V) {
|
||||
for notify := range p.GetSubscribers() {
|
||||
notify(event)
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type SimpleGrainPool[V any] struct {
|
||||
@@ -17,7 +17,8 @@ type SimpleGrainPool[V any] struct {
|
||||
grains map[uint64]Grain[V]
|
||||
mutationRegistry MutationRegistry
|
||||
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
|
||||
storage LogStorage[V]
|
||||
ttl time.Duration
|
||||
@@ -26,8 +27,8 @@ type SimpleGrainPool[V any] struct {
|
||||
// Cluster coordination --------------------------------------------------
|
||||
hostname string
|
||||
remoteMu sync.RWMutex
|
||||
remoteOwners map[uint64]Host
|
||||
remoteHosts map[string]Host
|
||||
remoteOwners map[uint64]Host[V]
|
||||
remoteHosts map[string]Host[V]
|
||||
//discardedHostHandler *DiscardedHostHandler
|
||||
|
||||
// House-keeping ---------------------------------------------------------
|
||||
@@ -37,7 +38,8 @@ type SimpleGrainPool[V any] struct {
|
||||
type GrainPoolConfig[V any] struct {
|
||||
Hostname string
|
||||
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
|
||||
PoolSize int
|
||||
MutationRegistry MutationRegistry
|
||||
@@ -51,11 +53,12 @@ func NewSimpleGrainPool[V any](config GrainPoolConfig[V]) (*SimpleGrainPool[V],
|
||||
storage: config.Storage,
|
||||
spawn: config.Spawn,
|
||||
spawnHost: config.SpawnHost,
|
||||
destroy: config.Destroy,
|
||||
ttl: config.TTL,
|
||||
poolSize: config.PoolSize,
|
||||
hostname: config.Hostname,
|
||||
remoteOwners: make(map[uint64]Host),
|
||||
remoteHosts: make(map[string]Host),
|
||||
remoteOwners: make(map[uint64]Host[V]),
|
||||
remoteHosts: make(map[string]Host[V]),
|
||||
}
|
||||
|
||||
p.purgeTicker = time.NewTicker(time.Minute)
|
||||
@@ -88,12 +91,15 @@ func (p *SimpleGrainPool[V]) purge() {
|
||||
for id, grain := range p.grains {
|
||||
if grain.GetLastAccess().Before(purgeLimit) {
|
||||
purgedIds = append(purgedIds, id)
|
||||
if err := p.destroy(grain); err != nil {
|
||||
log.Printf("failed to destroy grain %d: %v", id, err)
|
||||
}
|
||||
|
||||
delete(p.grains, id)
|
||||
}
|
||||
}
|
||||
p.localMu.Unlock()
|
||||
p.forAllHosts(func(remote Host) {
|
||||
p.forAllHosts(func(remote Host[V]) {
|
||||
remote.AnnounceExpiry(purgedIds)
|
||||
})
|
||||
|
||||
@@ -130,7 +136,6 @@ func (p *SimpleGrainPool[V]) HandleRemoteExpiry(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()
|
||||
remoteHost, exists := p.remoteHosts[host]
|
||||
p.remoteMu.RUnlock()
|
||||
@@ -162,7 +167,7 @@ func (p *SimpleGrainPool[V]) AddRemoteHost(host string) {
|
||||
p.AddRemote(host)
|
||||
}
|
||||
|
||||
func (p *SimpleGrainPool[V]) AddRemote(host string) (Host, error) {
|
||||
func (p *SimpleGrainPool[V]) AddRemote(host string) (Host[V], error) {
|
||||
if host == "" {
|
||||
return nil, fmt.Errorf("host is empty")
|
||||
}
|
||||
@@ -194,7 +199,7 @@ func (p *SimpleGrainPool[V]) AddRemote(host string) (Host, error) {
|
||||
return remote, nil
|
||||
}
|
||||
|
||||
func (p *SimpleGrainPool[V]) initializeRemote(remote Host) {
|
||||
func (p *SimpleGrainPool[V]) initializeRemote(remote Host[V]) {
|
||||
|
||||
remotesIds := remote.GetActorIds()
|
||||
|
||||
@@ -262,7 +267,7 @@ func (p *SimpleGrainPool[V]) IsKnown(host string) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
func (p *SimpleGrainPool[V]) pingLoop(remote Host) {
|
||||
func (p *SimpleGrainPool[V]) pingLoop(remote Host[V]) {
|
||||
remote.Ping()
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
@@ -310,14 +315,14 @@ func (p *SimpleGrainPool[V]) SendNegotiation() {
|
||||
p.remoteMu.RLock()
|
||||
hosts := make([]string, 0, len(p.remoteHosts)+1)
|
||||
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 {
|
||||
hosts = append(hosts, h)
|
||||
remotes = append(remotes, r)
|
||||
}
|
||||
p.remoteMu.RUnlock()
|
||||
|
||||
p.forAllHosts(func(remote Host) {
|
||||
p.forAllHosts(func(remote Host[V]) {
|
||||
knownByRemote, err := remote.Negotiate(hosts)
|
||||
|
||||
if err != nil {
|
||||
@@ -332,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()
|
||||
rh := maps.Clone(p.remoteHosts)
|
||||
p.remoteMu.RUnlock()
|
||||
@@ -360,7 +365,7 @@ func (p *SimpleGrainPool[V]) broadcastOwnership(ids []uint64) {
|
||||
return
|
||||
}
|
||||
|
||||
p.forAllHosts(func(rh Host) {
|
||||
p.forAllHosts(func(rh Host[V]) {
|
||||
rh.AnnounceOwnership(p.hostname, ids)
|
||||
})
|
||||
log.Printf("%s taking ownership of %d ids", p.hostname, len(ids))
|
||||
@@ -379,10 +384,11 @@ func (p *SimpleGrainPool[V]) getOrClaimGrain(ctx context.Context, id uint64) (Gr
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go p.broadcastOwnership([]uint64{id})
|
||||
p.localMu.Lock()
|
||||
p.grains[id] = grain
|
||||
p.localMu.Unlock()
|
||||
go p.broadcastOwnership([]uint64{id})
|
||||
|
||||
return grain, nil
|
||||
}
|
||||
|
||||
@@ -390,7 +396,7 @@ func (p *SimpleGrainPool[V]) getOrClaimGrain(ctx context.Context, id uint64) (Gr
|
||||
// var ErrNotOwner = fmt.Errorf("not owner")
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -414,8 +420,9 @@ func (p *SimpleGrainPool[V]) Apply(ctx context.Context, id uint64, mutation ...p
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &MutationResult[*V]{
|
||||
Result: result,
|
||||
|
||||
return &MutationResult[V]{
|
||||
Result: *result,
|
||||
Mutations: mutations,
|
||||
}, nil
|
||||
}
|
||||
@@ -430,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.
|
||||
func (p *SimpleGrainPool[V]) OwnerHost(id uint64) (Host, bool) {
|
||||
func (p *SimpleGrainPool[V]) OwnerHost(id uint64) (Host[V], bool) {
|
||||
p.remoteMu.RLock()
|
||||
defer p.remoteMu.RUnlock()
|
||||
owner, ok := p.remoteOwners[id]
|
||||
@@ -445,7 +452,7 @@ func (p *SimpleGrainPool[V]) Hostname() string {
|
||||
// Close notifies remotes that this host is shutting down.
|
||||
func (p *SimpleGrainPool[V]) Close() {
|
||||
|
||||
p.forAllHosts(func(rh Host) {
|
||||
p.forAllHosts(func(rh Host[V]) {
|
||||
rh.Close()
|
||||
})
|
||||
|
||||
|
||||
+3
-3
@@ -7,7 +7,7 @@ import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type StateStorage struct {
|
||||
@@ -34,14 +34,14 @@ func NewState(registry MutationRegistry) *StateStorage {
|
||||
|
||||
var ErrUnknownType = errors.New("unknown type")
|
||||
|
||||
func (s *StateStorage) Load(r io.Reader, onMessage func(msg proto.Message)) error {
|
||||
func (s *StateStorage) Load(r io.Reader, onMessage func(msg proto.Message, timeStamp time.Time)) error {
|
||||
var err error
|
||||
var evt *StorageEvent
|
||||
scanner := bufio.NewScanner(r)
|
||||
for err == nil {
|
||||
evt, err = s.Read(scanner)
|
||||
if err == nil {
|
||||
onMessage(evt.Mutation)
|
||||
onMessage(evt.Mutation, evt.TimeStamp)
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
|
||||
+82
-96
@@ -2,12 +2,11 @@ package cart
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
"git.tornberg.me/go-cart-actor/pkg/voucher"
|
||||
"git.k6n.net/go-cart-actor/pkg/voucher"
|
||||
"github.com/matst80/go-redis-inventory/pkg/inventory"
|
||||
)
|
||||
|
||||
// Legacy padded [16]byte CartId and its helper methods removed.
|
||||
@@ -23,37 +22,35 @@ type ItemMeta struct {
|
||||
Category3 string `json:"category3,omitempty"`
|
||||
Category4 string `json:"category4,omitempty"`
|
||||
Category5 string `json:"category5,omitempty"`
|
||||
SellerId string `json:"sellerId,omitempty"`
|
||||
SellerName string `json:"sellerName,omitempty"`
|
||||
Image string `json:"image,omitempty"`
|
||||
Outlet *string `json:"outlet,omitempty"`
|
||||
}
|
||||
|
||||
type CartItem struct {
|
||||
Id uint32 `json:"id"`
|
||||
ItemId uint32 `json:"itemId,omitempty"`
|
||||
ParentId *uint32 `json:"parentId,omitempty"`
|
||||
Sku string `json:"sku"`
|
||||
Price Price `json:"price"`
|
||||
TotalPrice Price `json:"totalPrice"`
|
||||
OrgPrice *Price `json:"orgPrice,omitempty"`
|
||||
Tax int
|
||||
Stock StockStatus `json:"stock"`
|
||||
Quantity int `json:"qty"`
|
||||
Discount *Price `json:"discount,omitempty"`
|
||||
Disclaimer string `json:"disclaimer,omitempty"`
|
||||
ArticleType string `json:"type,omitempty"`
|
||||
StoreId *string `json:"storeId,omitempty"`
|
||||
Meta *ItemMeta `json:"meta,omitempty"`
|
||||
SaleStatus string `json:"saleStatus"`
|
||||
}
|
||||
|
||||
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"`
|
||||
Id uint32 `json:"id"`
|
||||
ItemId uint32 `json:"itemId,omitempty"`
|
||||
ParentId *uint32 `json:"parentId,omitempty"`
|
||||
Sku string `json:"sku"`
|
||||
Price Price `json:"price"`
|
||||
TotalPrice Price `json:"totalPrice"`
|
||||
SellerId string `json:"sellerId,omitempty"`
|
||||
OrgPrice *Price `json:"orgPrice,omitempty"`
|
||||
Cgm string `json:"cgm,omitempty"`
|
||||
Tax int
|
||||
Stock uint16 `json:"stock"`
|
||||
Quantity uint16 `json:"qty"`
|
||||
Discount *Price `json:"discount,omitempty"`
|
||||
Disclaimer string `json:"disclaimer,omitempty"`
|
||||
ArticleType string `json:"type,omitempty"`
|
||||
StoreId *string `json:"storeId,omitempty"`
|
||||
Meta *ItemMeta `json:"meta,omitempty"`
|
||||
SaleStatus string `json:"saleStatus"`
|
||||
Marking *Marking `json:"marking,omitempty"`
|
||||
SubscriptionDetailsId string `json:"subscriptionDetailsId,omitempty"`
|
||||
OrderReference string `json:"orderReference,omitempty"`
|
||||
IsSubscribed bool `json:"isSubscribed,omitempty"`
|
||||
ReservationEndTime *time.Time `json:"reservationEndTime,omitempty"`
|
||||
}
|
||||
|
||||
type CartNotification struct {
|
||||
@@ -71,26 +68,52 @@ type SubscriptionDetails struct {
|
||||
Meta json.RawMessage `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type Notice struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Message string `json:"message"`
|
||||
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 uint32 `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type CartGrain struct {
|
||||
mu sync.RWMutex
|
||||
lastItemId uint32
|
||||
lastDeliveryId uint32
|
||||
lastVoucherId uint32
|
||||
lastAccess time.Time
|
||||
lastChange time.Time // unix seconds of last successful mutation (replay sets from event ts)
|
||||
userId string
|
||||
Id CartId `json:"id"`
|
||||
Items []*CartItem `json:"items"`
|
||||
TotalPrice *Price `json:"totalPrice"`
|
||||
TotalDiscount *Price `json:"totalDiscount"`
|
||||
Deliveries []*CartDelivery `json:"deliveries,omitempty"`
|
||||
Processing bool `json:"processing"`
|
||||
PaymentInProgress bool `json:"paymentInProgress"`
|
||||
OrderReference string `json:"orderReference,omitempty"`
|
||||
PaymentStatus string `json:"paymentStatus,omitempty"`
|
||||
mu sync.RWMutex
|
||||
lastItemId uint32
|
||||
lastVoucherId uint32
|
||||
lastAccess time.Time
|
||||
lastChange time.Time // unix seconds of last successful mutation (replay sets from event ts)
|
||||
userId string
|
||||
Currency string `json:"currency"`
|
||||
Language string `json:"language"`
|
||||
Version uint `json:"version"`
|
||||
InventoryReserved bool `json:"inventoryReserved"`
|
||||
Id CartId `json:"id"`
|
||||
Items []*CartItem `json:"items"`
|
||||
TotalPrice *Price `json:"totalPrice"`
|
||||
TotalDiscount *Price `json:"totalDiscount"`
|
||||
Processing bool `json:"processing"`
|
||||
//PaymentInProgress uint16 `json:"paymentInProgress"`
|
||||
OrderReference string `json:"orderReference,omitempty"`
|
||||
|
||||
Vouchers []*Voucher `json:"vouchers,omitempty"`
|
||||
Notifications []CartNotification `json:"cartNotification,omitempty"`
|
||||
SubscriptionDetails map[string]*SubscriptionDetails `json:"subscriptionDetails,omitempty"`
|
||||
|
||||
//CheckoutOrderId string `json:"checkoutOrderId,omitempty"`
|
||||
CheckoutStatus *CartPaymentStatus `json:"checkoutStatus,omitempty"`
|
||||
//CheckoutCountry string `json:"checkoutCountry,omitempty"`
|
||||
}
|
||||
|
||||
type Voucher struct {
|
||||
@@ -153,13 +176,11 @@ func (v *Voucher) AppliesTo(cart *CartGrain) ([]*CartItem, bool) {
|
||||
func NewCartGrain(id uint64, ts time.Time) *CartGrain {
|
||||
return &CartGrain{
|
||||
lastItemId: 0,
|
||||
lastDeliveryId: 0,
|
||||
lastVoucherId: 0,
|
||||
lastAccess: ts,
|
||||
lastChange: ts,
|
||||
TotalDiscount: NewPrice(),
|
||||
Vouchers: []*Voucher{},
|
||||
Deliveries: []*CartDelivery{},
|
||||
Id: CartId(id),
|
||||
Items: []*CartItem{},
|
||||
TotalPrice: NewPrice(),
|
||||
@@ -184,37 +205,23 @@ func (c *CartGrain) GetCurrentState() (*CartGrain, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *CartGrain) HandleInventoryChange(change inventory.InventoryChange) {
|
||||
for _, item := range c.Items {
|
||||
l := "se"
|
||||
if item.StoreId != nil {
|
||||
l = *item.StoreId
|
||||
}
|
||||
if item.Sku == change.SKU && change.StockLocationID == l {
|
||||
item.Stock = uint16(change.Value)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CartGrain) GetState() ([]byte, error) {
|
||||
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) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
@@ -226,26 +233,6 @@ func (c *CartGrain) FindItemWithSku(sku string) (*CartItem, bool) {
|
||||
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() {
|
||||
c.TotalPrice = NewPrice()
|
||||
c.TotalDiscount = NewPrice()
|
||||
@@ -270,9 +257,7 @@ func (c *CartGrain) UpdateTotals() {
|
||||
c.TotalPrice.Add(*rowTotal)
|
||||
|
||||
}
|
||||
for _, delivery := range c.Deliveries {
|
||||
c.TotalPrice.Add(delivery.Price)
|
||||
}
|
||||
|
||||
for _, voucher := range c.Vouchers {
|
||||
_, ok := voucher.AppliesTo(c)
|
||||
voucher.Applied = false
|
||||
@@ -287,4 +272,5 @@ func (c *CartGrain) UpdateTotals() {
|
||||
c.TotalPrice.Subtract(*value)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,53 +1,86 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"git.tornberg.me/go-cart-actor/pkg/actor"
|
||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
"context"
|
||||
"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.RegisterMutations(
|
||||
actor.NewMutation(AddItem, func() *messages.AddItem {
|
||||
return &messages.AddItem{}
|
||||
}),
|
||||
actor.NewMutation(ChangeQuantity, func() *messages.ChangeQuantity {
|
||||
return &messages.ChangeQuantity{}
|
||||
}),
|
||||
actor.NewMutation(RemoveItem, func() *messages.RemoveItem {
|
||||
return &messages.RemoveItem{}
|
||||
}),
|
||||
actor.NewMutation(InitializeCheckout, func() *messages.InitializeCheckout {
|
||||
return &messages.InitializeCheckout{}
|
||||
}),
|
||||
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(PreConditionFailed, func() *messages.PreConditionFailed {
|
||||
return &messages.PreConditionFailed{}
|
||||
}),
|
||||
actor.NewMutation(context.AddItem),
|
||||
actor.NewMutation(context.ChangeQuantity),
|
||||
actor.NewMutation(context.RemoveItem),
|
||||
actor.NewMutation(ClearCart),
|
||||
actor.NewMutation(AddVoucher),
|
||||
actor.NewMutation(RemoveVoucher),
|
||||
actor.NewMutation(UpsertSubscriptionDetails),
|
||||
actor.NewMutation(SetUserId),
|
||||
actor.NewMutation(LineItemMarking),
|
||||
actor.NewMutation(RemoveLineItemMarking),
|
||||
actor.NewMutation(SubscriptionAdded),
|
||||
// actor.NewMutation(SubscriptionRemoved),
|
||||
)
|
||||
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"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||
)
|
||||
|
||||
// cart_id.go
|
||||
@@ -34,7 +36,7 @@ import (
|
||||
//
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type CartId uint64
|
||||
type CartId actor.GrainId
|
||||
|
||||
const base62Alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"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
|
||||
@@ -12,15 +17,17 @@ import (
|
||||
// This replaces the legacy switch-based logic previously found in CartGrain.Apply.
|
||||
//
|
||||
// Behavior:
|
||||
// * Validates quantity > 0
|
||||
// * If an item with same SKU exists -> increases quantity
|
||||
// * Else creates a new CartItem with computed tax amounts
|
||||
// * Totals recalculated automatically via WithTotals()
|
||||
// - Validates quantity > 0
|
||||
// - If an item with same SKU exists -> increases quantity
|
||||
// - Else creates a new CartItem with computed tax amounts
|
||||
// - Totals recalculated automatically via WithTotals()
|
||||
//
|
||||
// NOTE: Any future field additions in messages.AddItem that affect pricing / tax
|
||||
// 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 {
|
||||
return fmt.Errorf("AddItem: nil payload")
|
||||
}
|
||||
@@ -38,12 +45,23 @@ func AddItem(g *CartGrain, m *messages.AddItem) error {
|
||||
if !sameStore {
|
||||
continue
|
||||
}
|
||||
existing.Quantity += int(m.Quantity)
|
||||
existing.Stock = StockStatus(m.Stock)
|
||||
if c.UseReservations(existing) {
|
||||
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.StoreId == nil && m.StoreId != nil {
|
||||
existing.StoreId = m.StoreId
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -58,10 +76,15 @@ func AddItem(g *CartGrain, m *messages.AddItem) error {
|
||||
|
||||
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,
|
||||
ItemId: uint32(m.ItemId),
|
||||
Quantity: int(m.Quantity),
|
||||
Quantity: uint16(m.Quantity),
|
||||
Sku: m.Sku,
|
||||
Tax: int(taxRate * 100),
|
||||
Meta: &ItemMeta{
|
||||
@@ -74,23 +97,38 @@ func AddItem(g *CartGrain, m *messages.AddItem) error {
|
||||
Category4: m.Category4,
|
||||
Category5: m.Category5,
|
||||
Outlet: m.Outlet,
|
||||
SellerId: m.SellerId,
|
||||
SellerName: m.SellerName,
|
||||
},
|
||||
SellerId: m.SellerId,
|
||||
Cgm: m.Cgm,
|
||||
SaleStatus: m.SaleStatus,
|
||||
ParentId: m.ParentId,
|
||||
|
||||
Price: *pricePerItem,
|
||||
TotalPrice: *MultiplyPrice(*pricePerItem, int64(m.Quantity)),
|
||||
|
||||
Stock: StockStatus(m.Stock),
|
||||
Stock: uint16(m.Stock),
|
||||
Disclaimer: m.Disclaimer,
|
||||
|
||||
OrgPrice: getOrgPrice(m.OrgPrice, taxRate),
|
||||
ArticleType: m.ArticleType,
|
||||
|
||||
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()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package cart
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"git.tornberg.me/go-cart-actor/pkg/actor"
|
||||
"git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||
)
|
||||
|
||||
func RemoveVoucher(g *CartGrain, m *messages.RemoveVoucher) error {
|
||||
@@ -15,6 +15,9 @@ func RemoveVoucher(g *CartGrain, m *messages.RemoveVoucher) error {
|
||||
StatusCode: 400,
|
||||
}
|
||||
}
|
||||
if g.CheckoutStatus != nil {
|
||||
return ErrPaymentInProgress
|
||||
}
|
||||
|
||||
if !slices.ContainsFunc(g.Vouchers, func(v *Voucher) bool {
|
||||
return v.Id == m.Id
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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
|
||||
@@ -26,11 +29,13 @@ import (
|
||||
// (If strict locking is required around every mutation, wrap logic in
|
||||
// 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 {
|
||||
return fmt.Errorf("ChangeQuantity: nil payload")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
foundIndex := -1
|
||||
for i, it := range g.Items {
|
||||
if it.Id == uint32(m.Id) {
|
||||
@@ -44,12 +49,37 @@ func ChangeQuantity(g *CartGrain, m *messages.ChangeQuantity) error {
|
||||
|
||||
if m.Quantity <= 0 {
|
||||
// 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.UpdateTotals()
|
||||
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()
|
||||
|
||||
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,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
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||
)
|
||||
|
||||
func LineItemMarking(grain *CartGrain, req *messages.LineItemMarking) error {
|
||||
for i, item := range grain.Items {
|
||||
if item.Id == req.Id {
|
||||
grain.Items[i].Marking = &Marking{
|
||||
Type: req.Type,
|
||||
Text: req.Marking,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("item with ID %d not found", req.Id)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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
|
||||
@@ -22,10 +25,11 @@ import (
|
||||
// - 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.
|
||||
|
||||
func RemoveItem(g *CartGrain, m *messages.RemoveItem) error {
|
||||
func (c *CartMutationContext) RemoveItem(g *CartGrain, m *messages.RemoveItem) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("RemoveItem: nil payload")
|
||||
}
|
||||
|
||||
targetID := uint32(m.Id)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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.UpdateTotals()
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||
)
|
||||
|
||||
func RemoveLineItemMarking(grain *CartGrain, req *messages.RemoveLineItemMarking) error {
|
||||
|
||||
for i, item := range grain.Items {
|
||||
if item.Id == req.Id {
|
||||
grain.Items[i].Marking = nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("item with ID %d not found", req.Id)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||
)
|
||||
|
||||
func SetUserId(grain *CartGrain, req *messages.SetUserId) error {
|
||||
if req.UserId == "" {
|
||||
return errors.New("user ID cannot be empty")
|
||||
}
|
||||
grain.userId = req.UserId
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||
)
|
||||
|
||||
func SubscriptionAdded(grain *CartGrain, req *messages.SubscriptionAdded) error {
|
||||
|
||||
for i, item := range grain.Items {
|
||||
if item.Id == req.ItemId {
|
||||
grain.Items[i].SubscriptionDetailsId = req.DetailsId
|
||||
grain.Items[i].OrderReference = req.OrderReference
|
||||
grain.Items[i].IsSubscribed = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("item with ID %d not found", req.ItemId)
|
||||
}
|
||||
@@ -1,526 +0,0 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
|
||||
"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 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",
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
func TestSubscriptionDetailsJSONValidation(t *testing.T) {
|
||||
reg := newRegistry()
|
||||
g := newTestGrain()
|
||||
|
||||
// Valid JSON on create
|
||||
jsonStr := `{"offeringCode": "OFFJSON", "signingType": "TYPEJSON", "data": {"value": "eyJvayI6dHJ1ZX0="}}`
|
||||
var validCreate messages.UpsertSubscriptionDetails
|
||||
if err := json.Unmarshal([]byte(jsonStr), &validCreate); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
applyOK(t, reg, g, &validCreate)
|
||||
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) != `{"ok":true}` {
|
||||
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", "data": {"value": ""}}`, 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))
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||
)
|
||||
|
||||
func UpsertSubscriptionDetails(g *CartGrain, m *messages.UpsertSubscriptionDetails) error {
|
||||
@@ -13,33 +12,17 @@ func UpsertSubscriptionDetails(g *CartGrain, m *messages.UpsertSubscriptionDetai
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper to get data bytes from Data field
|
||||
getDataBytes := func(data *anypb.Any) ([]byte, error) {
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if len(data.Value) > 0 {
|
||||
return data.Value, nil
|
||||
}
|
||||
if data.TypeUrl != "" {
|
||||
return []byte(data.TypeUrl), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
metaBytes := m.Data.GetValue()
|
||||
|
||||
// Create new subscription details when Id is nil.
|
||||
if m.Id == nil {
|
||||
// Validate JSON if provided.
|
||||
var meta json.RawMessage
|
||||
dataBytes, err := getDataBytes(m.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dataBytes != nil {
|
||||
if !json.Valid(dataBytes) {
|
||||
if metaBytes != nil {
|
||||
if !json.Valid(metaBytes) {
|
||||
return fmt.Errorf("subscription details invalid json")
|
||||
}
|
||||
meta = json.RawMessage(dataBytes)
|
||||
meta = json.RawMessage(metaBytes)
|
||||
}
|
||||
|
||||
id := MustNewCartId().String()
|
||||
@@ -57,7 +40,7 @@ func UpsertSubscriptionDetails(g *CartGrain, m *messages.UpsertSubscriptionDetai
|
||||
// Update existing entry.
|
||||
existing, ok := g.SubscriptionDetails[*m.Id]
|
||||
if !ok {
|
||||
return fmt.Errorf("subscription details not found")
|
||||
return fmt.Errorf("subscription details with id %s not found", *m.Id)
|
||||
}
|
||||
changed := false
|
||||
if m.OfferingCode != "" {
|
||||
@@ -68,15 +51,11 @@ func UpsertSubscriptionDetails(g *CartGrain, m *messages.UpsertSubscriptionDetai
|
||||
existing.SigningType = m.SigningType
|
||||
changed = true
|
||||
}
|
||||
dataBytes, err := getDataBytes(m.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dataBytes != nil {
|
||||
if !json.Valid(dataBytes) {
|
||||
if metaBytes != nil {
|
||||
if !json.Valid(metaBytes) {
|
||||
return fmt.Errorf("subscription details invalid json")
|
||||
}
|
||||
existing.Meta = dataBytes
|
||||
existing.Meta = json.RawMessage(metaBytes)
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
|
||||
@@ -87,6 +87,34 @@ func (p Price) MarshalJSON() ([]byte, error) {
|
||||
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
|
||||
// unnecessary decimals ("25", "12.5").
|
||||
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 (
|
||||
"fmt"
|
||||
|
||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||
)
|
||||
|
||||
// mutation_order_created.go
|
||||
@@ -11,21 +11,20 @@ import (
|
||||
// Registers the OrderCreated mutation.
|
||||
//
|
||||
// This mutation represents the completion (or state transition) of an order
|
||||
// initiated earlier via InitializeCheckout / external Klarna processing.
|
||||
// It finalizes (or updates) the cart's order metadata.
|
||||
// initiated earlier via InitializeCheckout / external processing.
|
||||
// It finalizes (or updates) the checkout's order metadata.
|
||||
//
|
||||
// Behavior:
|
||||
// - Validates payload non-nil and OrderId not empty.
|
||||
// - Sets (or overwrites) OrderReference with the provided OrderId.
|
||||
// - Sets PaymentStatus from payload.Status.
|
||||
// - Marks PaymentInProgress = false (checkout flow finished / acknowledged).
|
||||
// - Does NOT adjust monetary totals (no WithTotals()).
|
||||
// - Sets Status from payload.Status.
|
||||
// - Sets OrderId if not already set.
|
||||
// - Does NOT adjust monetary totals.
|
||||
//
|
||||
// Notes / Future Extensions:
|
||||
// - If multiple order completion events can arrive (e.g., retries / webhook
|
||||
// replays), this handler is idempotent: it simply overwrites fields.
|
||||
// - 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.
|
||||
//
|
||||
// Concurrency:
|
||||
@@ -33,16 +32,18 @@ import (
|
||||
// per grain. If out-of-order events are possible, embed versioning or
|
||||
// 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 {
|
||||
return fmt.Errorf("OrderCreated: nil payload")
|
||||
return fmt.Errorf("HandleOrderCreated: nil payload")
|
||||
}
|
||||
if m.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
|
||||
}
|
||||
@@ -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 (
|
||||
"fmt"
|
||||
|
||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||
)
|
||||
|
||||
// mutation_remove_delivery.go
|
||||
@@ -13,20 +13,10 @@ import (
|
||||
// Behavior:
|
||||
// - Removes the delivery entry whose Id == payload.Id.
|
||||
// - If not found, returns an error.
|
||||
// - Cart totals are recalculated (WithTotals) after removal.
|
||||
// - Items previously associated with that delivery simply become "without delivery";
|
||||
// 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 {
|
||||
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)
|
||||
g.Deliveries = append(g.Deliveries[:index], g.Deliveries[index+1:]...)
|
||||
g.UpdateTotals()
|
||||
return nil
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package cart
|
||||
package checkout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
messages "git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||
)
|
||||
|
||||
// mutation_set_pickup_point.go
|
||||
@@ -29,34 +29,23 @@ import (
|
||||
// - Track history / audit of pickup point changes
|
||||
// - 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 {
|
||||
return fmt.Errorf("SetPickupPoint: nil payload")
|
||||
}
|
||||
|
||||
for _, d := range g.Deliveries {
|
||||
if d.Id == uint32(m.DeliveryId) {
|
||||
d.PickupPoint = &messages.PickupPoint{
|
||||
Id: m.Id,
|
||||
Name: m.Name,
|
||||
Address: m.Address,
|
||||
City: m.City,
|
||||
Zip: m.Zip,
|
||||
Country: m.Country,
|
||||
d.PickupPoint = &PickupPoint{
|
||||
Id: m.PickupPoint.Id,
|
||||
Name: m.PickupPoint.Name,
|
||||
Address: m.PickupPoint.Address,
|
||||
City: m.PickupPoint.City,
|
||||
Zip: m.PickupPoint.Zip,
|
||||
Country: m.PickupPoint.Country,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
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 {
|
||||
ctx context.Context
|
||||
client *kubernetes.Clientset
|
||||
ctx context.Context
|
||||
client *kubernetes.Clientset
|
||||
listOptions metav1.ListOptions
|
||||
}
|
||||
|
||||
func (k *K8sDiscovery) Discover() ([]string, error) {
|
||||
return k.DiscoverInNamespace("")
|
||||
}
|
||||
func (k *K8sDiscovery) DiscoverInNamespace(namespace string) ([]string, error) {
|
||||
pods, err := k.client.CoreV1().Pods(namespace).List(k.ctx, metav1.ListOptions{
|
||||
LabelSelector: "actor-pool=cart",
|
||||
})
|
||||
pods, err := k.client.CoreV1().Pods(namespace).List(k.ctx, k.listOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hosts := make([]string, 0, len(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
|
||||
}
|
||||
|
||||
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) {
|
||||
timeout := int64(30)
|
||||
ipsThatAreReady := make(map[string]bool)
|
||||
m := sync.Mutex{}
|
||||
watcherFn := func(options metav1.ListOptions) (watch.Interface, error) {
|
||||
return k.client.CoreV1().Pods("").Watch(k.ctx, metav1.ListOptions{
|
||||
LabelSelector: "actor-pool=cart",
|
||||
TimeoutSeconds: &timeout,
|
||||
})
|
||||
return k.client.CoreV1().Pods("").Watch(k.ctx, k.listOptions)
|
||||
}
|
||||
watcher, err := toolsWatch.NewRetryWatcherWithContext(k.ctx, "1", &cache.ListWatch{WatchFunc: watcherFn})
|
||||
if err != nil {
|
||||
@@ -54,9 +57,7 @@ func (k *K8sDiscovery) Watch() (<-chan HostChange, error) {
|
||||
for event := range watcher.ResultChan() {
|
||||
|
||||
pod := event.Object.(*v1.Pod)
|
||||
isReady := slices.ContainsFunc(pod.Status.Conditions, func(condition v1.PodCondition) bool {
|
||||
return condition.Type == v1.PodReady && condition.Status == v1.ConditionTrue
|
||||
})
|
||||
isReady := hasReadyCondition(pod)
|
||||
m.Lock()
|
||||
oldState := ipsThatAreReady[pod.Status.PodIP]
|
||||
ipsThatAreReady[pod.Status.PodIP] = isReady
|
||||
@@ -76,9 +77,10 @@ func (k *K8sDiscovery) Watch() (<-chan HostChange, error) {
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func NewK8sDiscovery(client *kubernetes.Clientset) *K8sDiscovery {
|
||||
func NewK8sDiscovery(client *kubernetes.Clientset, listOptions metav1.ListOptions) *K8sDiscovery {
|
||||
return &K8sDiscovery{
|
||||
ctx: context.Background(),
|
||||
client: client,
|
||||
ctx: context.Background(),
|
||||
client: client,
|
||||
listOptions: listOptions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
@@ -17,7 +18,9 @@ func TestDiscovery(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("Error creating client: %v", err)
|
||||
}
|
||||
d := NewK8sDiscovery(client)
|
||||
d := NewK8sDiscovery(client, metav1.ListOptions{
|
||||
LabelSelector: "app",
|
||||
})
|
||||
res, err := d.DiscoverInNamespace("")
|
||||
if err != nil {
|
||||
t.Errorf("Error discovering: %v", err)
|
||||
@@ -36,7 +39,9 @@ func TestWatch(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("Error creating client: %v", err)
|
||||
}
|
||||
d := NewK8sDiscovery(client)
|
||||
d := NewK8sDiscovery(client, metav1.ListOptions{
|
||||
LabelSelector: "app",
|
||||
})
|
||||
ch, err := d.Watch()
|
||||
if err != nil {
|
||||
t.Errorf("Error watching: %v", err)
|
||||
|
||||
@@ -9,3 +9,9 @@ type Discovery interface {
|
||||
Discover() ([]string, 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"
|
||||
"time"
|
||||
|
||||
"git.tornberg.me/go-cart-actor/pkg/cart"
|
||||
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||
)
|
||||
|
||||
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.
|
||||
type PromotionItem struct {
|
||||
SKU string
|
||||
Quantity int
|
||||
Quantity uint16
|
||||
Category string
|
||||
PriceIncVat int64
|
||||
}
|
||||
@@ -36,7 +36,7 @@ type PromotionItem struct {
|
||||
// customer/order metadata.
|
||||
type PromotionEvalContext struct {
|
||||
CartTotalIncVat int64
|
||||
TotalItemQuantity int
|
||||
TotalItemQuantity uint32
|
||||
Items []PromotionItem
|
||||
CustomerSegment string
|
||||
CustomerLifetimeValue float64
|
||||
@@ -89,7 +89,7 @@ func NewContextFromCart(g *cart.CartGrain, opts ...ContextOption) *PromotionEval
|
||||
Category: strings.ToLower(category),
|
||||
PriceIncVat: it.Price.IncVat,
|
||||
})
|
||||
ctx.TotalItemQuantity += it.Quantity
|
||||
ctx.TotalItemQuantity += uint32(it.Quantity)
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(ctx)
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.tornberg.me/go-cart-actor/pkg/cart"
|
||||
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||
)
|
||||
|
||||
// --- Helpers ---------------------------------------------------------------
|
||||
@@ -58,7 +58,7 @@ func (t *testTracer) Count(name string) int {
|
||||
func makeCart(totalOverride int64, items []struct {
|
||||
sku string
|
||||
category string
|
||||
qty int
|
||||
qty uint16
|
||||
priceInc int64
|
||||
}) *cart.CartGrain {
|
||||
g := cart.NewCartGrain(1, time.Now())
|
||||
@@ -92,7 +92,7 @@ func TestEvaluateRuleBasicAND(t *testing.T) {
|
||||
g := makeCart(12000, []struct {
|
||||
sku string
|
||||
category string
|
||||
qty int
|
||||
qty uint16
|
||||
priceInc int64
|
||||
}{
|
||||
{"SKU-1", "summer", 2, 3000},
|
||||
@@ -226,7 +226,7 @@ func TestEvaluateProductCategoryIN(t *testing.T) {
|
||||
g := makeCart(-1, []struct {
|
||||
sku string
|
||||
category string
|
||||
qty int
|
||||
qty uint16
|
||||
priceInc int64
|
||||
}{
|
||||
{"A", "shoes", 1, 5000},
|
||||
@@ -268,7 +268,7 @@ func TestEvaluateGroupOR(t *testing.T) {
|
||||
g := makeCart(3000, []struct {
|
||||
sku string
|
||||
category string
|
||||
qty int
|
||||
qty uint16
|
||||
priceInc int64
|
||||
}{
|
||||
{"ONE", "x", 1, 3000},
|
||||
|
||||
+114
-15
@@ -1,7 +1,9 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -9,17 +11,20 @@ import (
|
||||
"net/http"
|
||||
"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/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"google.golang.org/grpc"
|
||||
"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
|
||||
// interaction.
|
||||
type RemoteHost struct {
|
||||
type RemoteHost[V any] struct {
|
||||
host string
|
||||
httpBase string
|
||||
conn *grpc.ClientConn
|
||||
@@ -37,7 +42,34 @@ var (
|
||||
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)
|
||||
|
||||
@@ -58,7 +90,7 @@ func NewRemoteHost(host string) (*RemoteHost, error) {
|
||||
}
|
||||
client := &http.Client{Transport: transport, Timeout: 10 * time.Second}
|
||||
|
||||
return &RemoteHost{
|
||||
return &RemoteHost[V]{
|
||||
host: host,
|
||||
httpBase: fmt.Sprintf("http://%s:8080", host),
|
||||
conn: conn,
|
||||
@@ -69,18 +101,18 @@ func NewRemoteHost(host string) (*RemoteHost, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *RemoteHost) Name() string {
|
||||
func (h *RemoteHost[V]) Name() string {
|
||||
return h.host
|
||||
}
|
||||
|
||||
func (h *RemoteHost) Close() error {
|
||||
func (h *RemoteHost[V]) Close() error {
|
||||
if h.conn != nil {
|
||||
h.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *RemoteHost) Ping() bool {
|
||||
func (h *RemoteHost[V]) Ping() bool {
|
||||
var err error = errors.ErrUnsupported
|
||||
for err != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
@@ -88,7 +120,6 @@ func (h *RemoteHost) Ping() bool {
|
||||
cancel()
|
||||
if err != nil {
|
||||
h.missedPings++
|
||||
log.Printf("Ping %s failed (%d) %v", h.host, h.missedPings, err)
|
||||
}
|
||||
if !h.IsHealthy() {
|
||||
return false
|
||||
@@ -100,7 +131,71 @@ func (h *RemoteHost) Ping() bool {
|
||||
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)
|
||||
defer cancel()
|
||||
|
||||
@@ -116,7 +211,7 @@ func (h *RemoteHost) Negotiate(knownHosts []string) ([]string, error) {
|
||||
return resp.Hosts, nil
|
||||
}
|
||||
|
||||
func (h *RemoteHost) GetActorIds() []uint64 {
|
||||
func (h *RemoteHost[V]) GetActorIds() []uint64 {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
reply, err := h.controlClient.GetLocalActorIds(ctx, &messages.Empty{})
|
||||
@@ -128,7 +223,7 @@ func (h *RemoteHost) GetActorIds() []uint64 {
|
||||
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{
|
||||
Host: ownerHost,
|
||||
Ids: uids,
|
||||
@@ -141,7 +236,7 @@ func (h *RemoteHost) AnnounceOwnership(ownerHost string, uids []uint64) {
|
||||
h.missedPings = 0
|
||||
}
|
||||
|
||||
func (h *RemoteHost) AnnounceExpiry(uids []uint64) {
|
||||
func (h *RemoteHost[V]) AnnounceExpiry(uids []uint64) {
|
||||
_, err := h.controlClient.AnnounceExpiry(context.Background(), &messages.ExpiryAnnounce{
|
||||
Host: h.host,
|
||||
Ids: uids,
|
||||
@@ -154,7 +249,7 @@ func (h *RemoteHost) AnnounceExpiry(uids []uint64) {
|
||||
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())
|
||||
|
||||
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)
|
||||
|
||||
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 {
|
||||
span.RecordError(err)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.tornberg.me/go-cart-actor/pkg/messages"
|
||||
messages "git.k6n.net/go-cart-actor/proto/cart"
|
||||
)
|
||||
|
||||
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
|
||||
// source: control_plane.proto
|
||||
|
||||
package messages
|
||||
package control_plane_messages
|
||||
|
||||
import (
|
||||
context "context"
|
||||
@@ -19,12 +19,14 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
ControlPlane_Ping_FullMethodName = "/messages.ControlPlane/Ping"
|
||||
ControlPlane_Negotiate_FullMethodName = "/messages.ControlPlane/Negotiate"
|
||||
ControlPlane_GetLocalActorIds_FullMethodName = "/messages.ControlPlane/GetLocalActorIds"
|
||||
ControlPlane_AnnounceOwnership_FullMethodName = "/messages.ControlPlane/AnnounceOwnership"
|
||||
ControlPlane_AnnounceExpiry_FullMethodName = "/messages.ControlPlane/AnnounceExpiry"
|
||||
ControlPlane_Closing_FullMethodName = "/messages.ControlPlane/Closing"
|
||||
ControlPlane_Ping_FullMethodName = "/control_plane_messages.ControlPlane/Ping"
|
||||
ControlPlane_Negotiate_FullMethodName = "/control_plane_messages.ControlPlane/Negotiate"
|
||||
ControlPlane_GetLocalActorIds_FullMethodName = "/control_plane_messages.ControlPlane/GetLocalActorIds"
|
||||
ControlPlane_AnnounceOwnership_FullMethodName = "/control_plane_messages.ControlPlane/AnnounceOwnership"
|
||||
ControlPlane_Apply_FullMethodName = "/control_plane_messages.ControlPlane/Apply"
|
||||
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.
|
||||
@@ -41,10 +43,12 @@ type ControlPlaneClient interface {
|
||||
GetLocalActorIds(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*ActorIdsReply, error)
|
||||
// Ownership announcement: first-touch claim broadcast (idempotent; best-effort).
|
||||
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.
|
||||
AnnounceExpiry(ctx context.Context, in *ExpiryAnnounce, opts ...grpc.CallOption) (*OwnerChangeAck, error)
|
||||
// Closing announces graceful shutdown so peers can proactively adjust.
|
||||
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 {
|
||||
@@ -95,6 +99,16 @@ func (c *controlPlaneClient) AnnounceOwnership(ctx context.Context, in *Ownershi
|
||||
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) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(OwnerChangeAck)
|
||||
@@ -115,6 +129,16 @@ func (c *controlPlaneClient) Closing(ctx context.Context, in *ClosingNotice, opt
|
||||
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.
|
||||
// All implementations must embed UnimplementedControlPlaneServer
|
||||
// for forward compatibility.
|
||||
@@ -129,10 +153,12 @@ type ControlPlaneServer interface {
|
||||
GetLocalActorIds(context.Context, *Empty) (*ActorIdsReply, error)
|
||||
// Ownership announcement: first-touch claim broadcast (idempotent; best-effort).
|
||||
AnnounceOwnership(context.Context, *OwnershipAnnounce) (*OwnerChangeAck, error)
|
||||
Apply(context.Context, *ApplyRequest) (*ApplyResult, error)
|
||||
// Expiry announcement: drop remote ownership hints when local TTL expires.
|
||||
AnnounceExpiry(context.Context, *ExpiryAnnounce) (*OwnerChangeAck, error)
|
||||
// Closing announces graceful shutdown so peers can proactively adjust.
|
||||
Closing(context.Context, *ClosingNotice) (*OwnerChangeAck, error)
|
||||
Get(context.Context, *GetRequest) (*GetReply, error)
|
||||
mustEmbedUnimplementedControlPlaneServer()
|
||||
}
|
||||
|
||||
@@ -155,12 +181,18 @@ func (UnimplementedControlPlaneServer) GetLocalActorIds(context.Context, *Empty)
|
||||
func (UnimplementedControlPlaneServer) AnnounceOwnership(context.Context, *OwnershipAnnounce) (*OwnerChangeAck, error) {
|
||||
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) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AnnounceExpiry not implemented")
|
||||
}
|
||||
func (UnimplementedControlPlaneServer) Closing(context.Context, *ClosingNotice) (*OwnerChangeAck, error) {
|
||||
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) testEmbeddedByValue() {}
|
||||
|
||||
@@ -254,6 +286,24 @@ func _ControlPlane_AnnounceOwnership_Handler(srv interface{}, ctx context.Contex
|
||||
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) {
|
||||
in := new(ExpiryAnnounce)
|
||||
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)
|
||||
}
|
||||
|
||||
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.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var ControlPlane_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "messages.ControlPlane",
|
||||
ServiceName: "control_plane_messages.ControlPlane",
|
||||
HandlerType: (*ControlPlaneServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
@@ -313,6 +381,10 @@ var ControlPlane_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "AnnounceOwnership",
|
||||
Handler: _ControlPlane_AnnounceOwnership_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Apply",
|
||||
Handler: _ControlPlane_Apply_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AnnounceExpiry",
|
||||
Handler: _ControlPlane_AnnounceExpiry_Handler,
|
||||
@@ -321,6 +393,10 @@ var ControlPlane_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "Closing",
|
||||
Handler: _ControlPlane_Closing_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Get",
|
||||
Handler: _ControlPlane_Get_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "control_plane.proto",
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user