diff --git a/GRPC-MIGRATION-PLAN.md b/GRPC-MIGRATION-PLAN.md
deleted file mode 100644
index e2fdc48..0000000
--- a/GRPC-MIGRATION-PLAN.md
+++ /dev/null
@@ -1,396 +0,0 @@
-# gRPC Migration Plan
-
-File: GRPC-MIGRATION-PLAN.md
-Author: (Generated plan)
-Status: Draft for review
-Target Release: Next major version (breaking change – no mixed compatibility)
-
----
-
-## 1. Overview
-
-This document describes the full migration of the current custom TCP frame-based protocol (both the cart mutation/state channel on port `1337` and the control plane on port `1338`) to gRPC. We will remove all legacy packet framing (`FrameWithPayload`, `RemoteGrain`, `GenericListener` handlers for these two ports) and replace them with two gRPC services:
-
-1. Cart Actor Service (mutations + state retrieval)
-2. Control Plane Service (cluster membership, negotiation, ownership change, lifecycle)
-
-We intentionally keep:
-- Internal `CartGrain` logic, message storage format, disk persistence, and JSON cart serialization.
-- Existing message type numeric mapping for backward compatibility with persisted event logs.
-- HTTP/REST API layer unchanged (it still consumes JSON state from the local/remote grain pipeline).
-
-We do NOT implement mixed-version compatibility; migration occurs atomically (cluster restart with new image).
-
----
-
-## 2. Goals
-
-- Remove custom binary frame protocol & simplify maintenance.
-- Provide clearer, strongly defined interfaces via `.proto` schemas.
-- Improve observability via gRPC interceptors (metrics & tracing hooks).
-- Reduce per-call overhead compared with the current manual connection pooling + handwritten framing (HTTP/2 multiplexing + connection reuse).
-- Prepare groundwork for future enhancements (streaming, typed state, event streaming) without rewriting again.
-
----
-
-## 3. Non-Goals (Phase 1)
-
-- Converting the cart state payload from JSON to a strongly typed proto.
-- Introducing authentication / mTLS (may be added later).
-- Changing persistence or replay format.
-- Changing the HTTP API contract.
-- Implementing streaming watchers or push updates.
-
----
-
-## 4. Architecture After Migration
-
-Ports:
-- `:1337` → gRPC CartActor service.
-- `:1338` → gRPC ControlPlane service.
-
-Each node:
-- Runs one gRPC server with both services (can use a single listener bound to two services or keep two separate listeners; we will keep two ports initially to minimize operational surprise, but they could be merged later).
-- Maintains a connection pool of `*grpc.ClientConn` objects keyed by remote hostname (one per remote host, reused for both services).
-
-Call Flow (Mutation):
-1. HTTP request hits `PoolServer`.
-2. `SyncedPool.getGrain(cartId)`:
- - Local: direct invocation.
- - Remote: uses `RemoteGrainGRPC` (new) which invokes `CartActor.Mutate`.
-3. Response JSON returned unchanged.
-
-Control Plane Flow:
-- Discovery (K8s watch) still triggers `AddRemote(host)`.
-- Instead of custom `Ping`, `Negotiate`, etc. via frames, call gRPC methods on `ControlPlane` service.
-- Ownership changes use `ConfirmOwner` RPC.
-
----
-
-## 5. Proto Design
-
-### 5.1 Cart Actor Proto (Envelope Pattern)
-
-We keep an envelope with `bytes payload` holding the serialized underlying cart mutation proto (existing types in `messages.proto`). This minimizes churn.
-
-Indented code block (proto sketch):
-
- syntax = "proto3";
- package cart;
- option go_package = "git.tornberg.me/go-cart-actor/proto;proto";
-
- enum MutationType {
- MUTATION_TYPE_UNSPECIFIED = 0;
- MUTATION_ADD_REQUEST = 1;
- MUTATION_ADD_ITEM = 2;
- MUTATION_REMOVE_ITEM = 4;
- MUTATION_REMOVE_DELIVERY = 5;
- MUTATION_CHANGE_QUANTITY = 6;
- MUTATION_SET_DELIVERY = 7;
- MUTATION_SET_PICKUP_POINT = 8;
- MUTATION_CREATE_CHECKOUT_ORDER = 9;
- MUTATION_SET_CART_ITEMS = 10;
- MUTATION_ORDER_COMPLETED = 11;
- }
-
- message MutationRequest {
- string cart_id = 1;
- MutationType type = 2;
- bytes payload = 3; // Serialized specific mutation proto
- int64 client_timestamp = 4; // Optional; server fills if zero
- }
-
- message MutationReply {
- int32 status_code = 1;
- bytes payload = 2; // JSON cart state or error string
- }
-
- message StateRequest {
- string cart_id = 1;
- }
-
- message StateReply {
- int32 status_code = 1;
- bytes payload = 2; // JSON cart state
- }
-
- service CartActor {
- rpc Mutate(MutationRequest) returns (MutationReply);
- rpc GetState(StateRequest) returns (StateReply);
- }
-
-### 5.2 Control Plane Proto
-
- syntax = "proto3";
- package control;
- option go_package = "git.tornberg.me/go-cart-actor/proto;proto";
-
- message Empty {}
-
- message PingReply {
- string host = 1;
- int64 unix_time = 2;
- }
-
- message NegotiateRequest {
- repeated string known_hosts = 1;
- }
- message NegotiateReply {
- repeated string hosts = 1; // Healthy hosts returned
- }
-
- message CartIdsReply {
- repeated string cart_ids = 1;
- }
-
- message OwnerChangeRequest {
- string cart_id = 1;
- string new_host = 2;
- }
- message OwnerChangeAck {
- bool accepted = 1;
- string message = 2;
- }
-
- message ClosingNotice {
- string host = 1;
- }
-
- service ControlPlane {
- rpc Ping(Empty) returns (PingReply);
- rpc Negotiate(NegotiateRequest) returns (NegotiateReply);
- rpc GetCartIds(Empty) returns (CartIdsReply);
- rpc ConfirmOwner(OwnerChangeRequest) returns (OwnerChangeAck);
- rpc Closing(ClosingNotice) returns (OwnerChangeAck);
- }
-
----
-
-## 6. Message Type Mapping
-
-| Legacy Constant | Numeric | New Enum Value |
-|-----------------|---------|-----------------------------|
-| AddRequestType | 1 | MUTATION_ADD_REQUEST |
-| AddItemType | 2 | MUTATION_ADD_ITEM |
-| RemoveItemType | 4 | MUTATION_REMOVE_ITEM |
-| RemoveDeliveryType | 5 | MUTATION_REMOVE_DELIVERY |
-| ChangeQuantityType | 6 | MUTATION_CHANGE_QUANTITY |
-| SetDeliveryType | 7 | MUTATION_SET_DELIVERY |
-| SetPickupPointType | 8 | MUTATION_SET_PICKUP_POINT |
-| CreateCheckoutOrderType | 9 | MUTATION_CREATE_CHECKOUT_ORDER |
-| SetCartItemsType | 10 | MUTATION_SET_CART_ITEMS |
-| OrderCompletedType | 11 | MUTATION_ORDER_COMPLETED |
-
-Persisted events keep original numeric codes; reconstruction simply casts to `MutationType`.
-
----
-
-## 7. Components To Remove / Replace
-
-Remove (after migration complete):
-- `remote-grain.go`
-- `rpc-server.go`
-- Any packet/frame-specific types solely used by the above (search: `FrameWithPayload`, `RemoteHandleMutation`, `RemoteGetState` where not reused by disk or internal logic).
-- The constants representing network frame types in `synced-pool.go` (RemoteNegotiate, AckChange, etc.) replaced by gRPC calls.
-- netpool usage for remote cart channel (control plane also no longer needs `Connection` abstraction).
-
-Retain (until reworked or optionally cleaned later):
-- `message.go` (for persistence)
-- `message-handler.go`
-- `cart-grain.go`
-- `messages.proto` (underlying mutation messages)
-- HTTP API server and REST handlers.
-
----
-
-## 8. New / Modified Components
-
-New files (planned):
-- `proto/cart_actor.proto`
-- `proto/control_plane.proto`
-- `grpc/cart_actor_server.go` (server impl)
-- `grpc/cart_actor_client.go` (client adapter implementing `Grain`)
-- `grpc/control_plane_server.go`
-- `grpc/control_plane_client.go`
-- `grpc/interceptors.go` (metrics, logging, optional tracing hooks)
-- `remote_grain_grpc.go` (adapter bridging existing interfaces)
-- `control_plane_adapter.go` (replaces frame handlers in `SyncedPool`)
-
-Modified:
-- `synced-pool.go` (remote host management now uses gRPC clients; negotiation logic updated)
-- `main.go` (initialize both gRPC services on startup)
-- `go.mod` (add `google.golang.org/grpc`)
-
----
-
-## 9. Step-by-Step Migration Plan
-
-1. Add proto files and generate Go code (`protoc --go_out --go-grpc_out`).
-2. Implement `CartActorServer`:
- - Translate `MutationRequest` to `Message`.
- - Use existing handler registry for payload encode/decode.
- - Return JSON cart state.
-3. Implement `CartActorClient` wrapper (`RemoteGrainGRPC`) implementing:
- - `HandleMessage`: Build envelope, call `Mutate`.
- - `GetCurrentState`: Call `GetState`.
-4. Implement `ControlPlaneServer` with methods:
- - `Ping`: returns host + time.
- - `Negotiate`: merge host lists; emulate old logic.
- - `GetCartIds`: iterate local grains.
- - `ConfirmOwner`: replicate quorum flow (accept always; error path for future).
- - `Closing`: schedule remote removal.
-5. Implement `ControlPlaneClient` used inside `SyncedPool.AddRemote`.
-6. Refactor `SyncedPool`:
- - Replace frame handlers registration with gRPC client calls.
- - Replace `Server.AddHandler(...)` start-up with launching gRPC server.
- - Implement periodic health checks using `Ping`.
-7. Remove old connection constructs for 1337/1338.
-8. Metrics:
- - Add unary interceptor capturing duration and status.
- - Replace packet counters with `cart_grpc_mutate_calls_total`, `cart_grpc_control_calls_total`, histograms for latency.
-9. Update `main.go` to start:
- - gRPC server(s).
- - HTTP server as before.
-10. Delete legacy files & update README build instructions.
-11. Load testing & profiling on Raspberry Pi hardware (or ARM emulation).
-12. Final cleanup & dead code removal (search for now-unused constants & structs).
-13. Tag release.
-
----
-
-## 10. Performance Considerations (Raspberry Pi Focus)
-
-- Single `*grpc.ClientConn` per remote host (HTTP/2 multiplexing) to reduce file descriptor and handshake overhead.
-- Use small keepalive pings (optional) only if connections drop; default may suffice.
-- Avoid reflection / dynamic dispatch in hot path: pre-build a mapping from `MutationType` to handler function.
-- Reuse byte buffers:
- - Implement a `sync.Pool` for mutation serialization to reduce GC pressure.
-- Enforce per-RPC deadlines (e.g. 300–400ms) to avoid pile-ups.
-- Backpressure:
- - Before dispatch: if local grain pool at capacity and target grain is remote, abort early with 503 to caller (optional).
-- Disable gRPC compression for small payloads (mutation messages are small). Condition compression if payload > threshold (e.g. 8KB).
-- Compile with `-ldflags="-s -w"` in production to reduce binary size (optional).
-- Enable `GOMAXPROCS` tuned to CPU cores; Pi often benefits from leaving default but monitor.
-- Use histograms with limited buckets to reduce Prometheus cardinality.
-
----
-
-## 11. Testing Strategy
-
-Unit:
-- Message type mapping tests (legacy -> enum).
-- Envelope roundtrip: Original proto -> payload -> gRPC -> server decode -> internal Message.
-
-Integration:
-- Two-node cluster simulation:
- - Mutate cart on Node A, ownership moves, verify remote access from Node B.
- - Quorum failure simulation (temporarily reject `ConfirmOwner`).
-- Control plane negotiation: start nodes in staggered order, assert final membership.
-
-Load/Perf:
-- Benchmark local mutation vs remote mutation latency.
-- High concurrency test (N goroutines each performing X mutations).
-- Memory profiling (ensure no large buffer retention).
-
-Failure Injection:
-- Kill a node mid-mutation; client call should timeout and not corrupt local state.
-- Simulated network partition: drop `Ping` replies; ensure host removal path triggers.
-
----
-
-## 12. Rollback Strategy
-
-Because no mixed-version compatibility is provided, rollback = redeploy previous version containing legacy protocol:
-1. Stop all new-version pods.
-2. Deploy old version cluster-wide.
-3. No data migration needed (event persistence unaffected).
-
-Note: Avoid partial upgrades; perform full rolling restart quickly to prevent split-brain (new nodes won’t talk to old nodes).
-
----
-
-## 13. Risks & Mitigations
-
-| Risk | Description | Mitigation |
-|------|-------------|------------|
-| Full-cluster restart required | No mixed compatibility | Schedule maintenance window |
-| gRPC adds CPU overhead | Envelope + marshaling cost | Buffer reuse, keep small messages uncompressed |
-| Ownership race | Timing differences after refactor | Add explicit logs + tests around `RequestOwnership` path |
-| Hidden dependency on frame-level status codes | Some code may assume `FrameWithPayload` fields | Wrap gRPC responses into minimal compatibility structs until fully removed |
-| Memory growth | Connection reuse & pooled buffers not implemented initially | Add `sync.Pool` & track memory via pprof early |
-
----
-
-## 14. Logging & Observability
-
-- Structured log entries for:
- - Ownership changes
- - Negotiation rounds
- - Remote spawn events
- - Mutation failures (with cart id, mutation type)
-- Metrics:
- - `cart_grpc_mutate_duration_seconds` (histogram)
- - `cart_grpc_mutate_errors_total`
- - `cart_grpc_control_duration_seconds`
- - `cart_remote_hosts` (gauge)
- - Retain existing grain counts.
-- Optional future: OpenTelemetry tracing (span per remote mutation).
-
----
-
-## 15. Future Enhancements (Post-Migration)
-
-- Replace JSON state with `CartState` proto and provide streaming watch API.
-- mTLS between nodes (certificate rotation via K8s Secret or SPIRE).
-- Distributed tracing integration.
-- Ownership leasing with TTL and optimistic renewal.
-- Delta replication or CRDT-based conflict resolution for experimentation.
-
----
-
-## 16. Task Breakdown & Estimates
-
-| Task | Estimate |
-|------|----------|
-| Proto definitions & generation | 0.5d |
-| CartActor server/client | 1.0d |
-| ControlPlane server/client | 1.0d |
-| SyncedPool refactor | 1.0d |
-| Metrics & interceptors | 0.5d |
-| Remove legacy code & cleanup | 0.5d |
-| Tests (unit + integration) | 1.5d |
-| Benchmark & tuning | 0.5–1.0d |
-| Total | ~6–7d |
-
----
-
-## 17. Open Questions (Confirm Before Implementation)
-
-1. Combine both services on a single port (simplify ops) or keep dual-port first? (Default here: keep dual, but easy to merge.)
-2. Minimum Go version remains 1.24.x—acceptable to add `google.golang.org/grpc` latest?
-3. Accept adding `sync.Pool` micro-optimizations in first pass or postpone?
-
----
-
-## 18. Acceptance Criteria
-
-- All previous integration tests (adjusted to gRPC) pass.
-- Cart operations (add, remove, delivery, checkout) function across at least a 2‑node cluster.
-- Control plane negotiation forms consistent host list.
-- Latency for a remote mutation does not degrade beyond an acceptable threshold (define baseline before merge).
-- Legacy networking code fully removed.
-
----
-
-## 19. Next Steps (If Approved)
-
-1. Implement proto files and commit.
-2. Scaffold server & client code.
-3. Refactor `SyncedPool` and `main.go`.
-4. Add metrics and tests.
-5. Run benchmark on target Pi hardware.
-6. Review & merge.
-
----
-
-End of Plan.
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..dafaef0
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,119 @@
+# ------------------------------------------------------------------------------
+# Makefile for go-cart-actor
+#
+# Key targets:
+# make protogen - Generate protobuf + gRPC code into proto/
+# make clean_proto - Remove generated proto *.pb.go files
+# make verify_proto - Ensure no stray root-level *.pb.go files exist
+# make build - Build the project
+# make test - Run tests (verbose)
+# make tidy - Run go mod tidy
+# make regen - Clean proto, regenerate, tidy, verify, build
+# make help - Show this help
+#
+# 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
+# ------------------------------------------------------------------------------
+
+MODULE_PATH := git.tornberg.me/go-cart-actor
+PROTO_DIR := proto
+PROTOS := $(PROTO_DIR)/messages.proto $(PROTO_DIR)/cart_actor.proto $(PROTO_DIR)/control_plane.proto
+
+# Allow override: make PROTOC=/path/to/protoc
+PROTOC ?= protoc
+
+# Tools (auto-detect; can override)
+PROTOC_GEN_GO ?= $(shell command -v protoc-gen-go 2>/dev/null)
+PROTOC_GEN_GO_GRPC ?= $(shell command -v protoc-gen-go-grpc 2>/dev/null)
+
+GO ?= go
+
+# Colors (optional)
+GREEN := \033[32m
+RED := \033[31m
+YELLOW := \033[33m
+RESET := \033[0m
+
+# ------------------------------------------------------------------------------
+
+.PHONY: protogen clean_proto verify_proto tidy build test regen help check_tools
+
+help:
+ @echo "Available targets:"
+ @echo " protogen Generate protobuf & gRPC code"
+ @echo " clean_proto Remove generated *.pb.go files in $(PROTO_DIR)"
+ @echo " verify_proto Ensure no root-level *.pb.go files (old layout)"
+ @echo " tidy Run go mod tidy"
+ @echo " build Build the module"
+ @echo " test Run tests (verbose)"
+ @echo " regen Clean proto, regenerate, tidy, verify, and build"
+ @echo " check_tools Verify protoc + plugins are installed"
+
+check_tools:
+ @if [ -z "$(PROTOC_GEN_GO)" ] || [ -z "$(PROTOC_GEN_GO_GRPC)" ]; then \
+ echo "$(RED)Missing protoc-gen-go or protoc-gen-go-grpc in PATH.$(RESET)"; \
+ echo "Install with:"; \
+ echo " go install google.golang.org/protobuf/cmd/protoc-gen-go@latest"; \
+ echo " go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest"; \
+ exit 1; \
+ fi
+ @if ! command -v "$(PROTOC)" >/dev/null 2>&1; then \
+ echo "$(RED)protoc not found. Install protoc (e.g. via package manager)$(RESET)"; \
+ exit 1; \
+ fi
+ @echo "$(GREEN)All required tools detected.$(RESET)"
+
+protogen: check_tools
+ @echo "$(YELLOW)Generating protobuf code (outputs -> ./proto)...$(RESET)"
+ $(PROTOC) -I $(PROTO_DIR) \
+ --go_out=./proto --go_opt=paths=source_relative \
+ --go-grpc_out=./proto --go-grpc_opt=paths=source_relative \
+ $(PROTOS)
+ @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
+ @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)"; \
+ ls -1 *.pb.go; \
+ exit 1; \
+ fi
+ @echo "$(GREEN)Proto layout OK (no root-level *.pb.go files).$(RESET)"
+
+tidy:
+ @echo "$(YELLOW)Running go mod tidy...$(RESET)"
+ $(GO) mod tidy
+ @echo "$(GREEN)tidy complete.$(RESET)"
+
+build:
+ @echo "$(YELLOW)Building...$(RESET)"
+ $(GO) build ./...
+ @echo "$(GREEN)Build success.$(RESET)"
+
+test:
+ @echo "$(YELLOW)Running tests...$(RESET)"
+ $(GO) test -v ./...
+ @echo "$(GREEN)Tests completed.$(RESET)"
+
+regen: clean_proto protogen tidy verify_proto build
+ @echo "$(GREEN)Full regenerate cycle complete.$(RESET)"
+
+# Utility: show proto sources and generated outputs
+print_proto:
+ @echo "Proto sources:"
+ @ls -1 $(PROTOS)
+ @echo ""
+ @echo "Generated files (if any):"
+ @ls -1 $(PROTO_DIR)/*pb.go 2>/dev/null || echo "(none)"
+
+# Prevent make from treating these as file targets if similarly named files appear.
+.SILENT: help check_tools protogen clean_proto verify_proto tidy build test regen print_proto
diff --git a/amqp-order-handler.go b/amqp-order-handler.go
deleted file mode 100644
index 776cfe7..0000000
--- a/amqp-order-handler.go
+++ /dev/null
@@ -1,83 +0,0 @@
-package main
-
-import (
- "log"
-
- amqp "github.com/rabbitmq/amqp091-go"
-)
-
-type AmqpOrderHandler struct {
- Url string
- connection *amqp.Connection
- //channel *amqp.Channel
-}
-
-const (
- topic = "order-placed"
-)
-
-func (t *AmqpOrderHandler) Connect() error {
-
- conn, err := amqp.DialConfig(t.Url, amqp.Config{
- //Vhost: "/",
- Properties: amqp.NewConnectionProperties(),
- })
-
- if err != nil {
- return err
- }
- t.connection = conn
- ch, err := conn.Channel()
- if err != nil {
- return err
- }
- defer ch.Close()
- if err := ch.ExchangeDeclare(
- topic, // name
- "topic", // type
- true, // durable
- false, // auto-delete
- false, // internal
- false, // noWait
- nil, // arguments
- ); err != nil {
- return err
- }
-
- if _, err = ch.QueueDeclare(
- topic, // name of the queue
- true, // durable
- false, // delete when unused
- false, // exclusive
- false, // noWait
- nil, // arguments
- ); err != nil {
- return err
- }
-
- return nil
-}
-
-func (t *AmqpOrderHandler) Close() error {
- log.Println("Closing master channel")
- return t.connection.Close()
- //return t.channel.Close()
-}
-
-func (t *AmqpOrderHandler) OrderCompleted(data []byte) error {
- ch, err := t.connection.Channel()
- if err != nil {
- return err
- }
- defer ch.Close()
- return ch.Publish(
- topic,
- topic,
- true,
- false,
- amqp.Publishing{
- ContentType: "application/json",
- Body: data,
- },
- )
-}
diff --git a/cart-grain.go b/cart-grain.go
index 9a08a62..ffca614 100644
--- a/cart-grain.go
+++ b/cart-grain.go
@@ -7,7 +7,6 @@ import (
"log"
"slices"
"sync"
- "time"
messages "git.tornberg.me/go-cart-actor/proto"
)
@@ -93,7 +92,6 @@ type CartGrain struct {
mu sync.RWMutex
lastItemId int
lastDeliveryId int
- storageMessages []Message
Id CartId `json:"id"`
Items []*CartItem `json:"items"`
TotalPrice int64 `json:"totalPrice"`
@@ -108,8 +106,8 @@ type CartGrain struct {
type Grain interface {
GetId() CartId
- HandleMessage(message *Message, isReplay bool) (*FrameWithPayload, error)
- GetCurrentState() (*FrameWithPayload, error)
+ Apply(content interface{}, isReplay bool) (*CartGrain, error)
+ GetCurrentState() (*CartGrain, error)
}
func (c *CartGrain) GetId() CartId {
@@ -117,20 +115,12 @@ func (c *CartGrain) GetId() CartId {
}
func (c *CartGrain) GetLastChange() int64 {
- if len(c.storageMessages) == 0 {
- return 0
- }
- return *c.storageMessages[len(c.storageMessages)-1].TimeStamp
+ // Legacy event log removed; return 0 to indicate no persisted mutation history.
+ return 0
}
-func (c *CartGrain) GetCurrentState() (*FrameWithPayload, error) {
- result, err := json.Marshal(c)
- if err != nil {
- ret := MakeFrameWithPayload(0, 400, []byte(err.Error()))
- return &ret, nil
- }
- ret := MakeFrameWithPayload(0, 200, result)
- return &ret, nil
+func (c *CartGrain) GetCurrentState() (*CartGrain, error) {
+ return c, nil
}
func getInt(data float64, ok bool) (int, error) {
@@ -201,30 +191,23 @@ func getItemData(sku string, qty int, country string) (*messages.AddItem, error)
}, nil
}
-func (c *CartGrain) AddItem(sku string, qty int, country string, storeId *string) (*FrameWithPayload, error) {
+func (c *CartGrain) AddItem(sku string, qty int, country string, storeId *string) (*CartGrain, error) {
cartItem, err := getItemData(sku, qty, country)
if err != nil {
return nil, err
}
cartItem.StoreId = storeId
- return c.HandleMessage(&Message{
- Type: 2,
- Content: cartItem,
- }, false)
+ return c.Apply(cartItem, false)
}
-func (c *CartGrain) GetStorageMessage(since int64) []StorableMessage {
- c.mu.RLock()
- defer c.mu.RUnlock()
- ret := make([]StorableMessage, 0)
+/*
+Legacy storage (event sourcing) removed in oneof refactor.
+Kept stub (commented) for potential future reintroduction using proto envelopes.
- for _, message := range c.storageMessages {
- if *message.TimeStamp > since {
- ret = append(ret, message)
- }
- }
- return ret
+func (c *CartGrain) GetStorageMessage(since int64) []interface{} {
+ return nil
}
+*/
func (c *CartGrain) GetState() ([]byte, error) {
return json.Marshal(c)
@@ -279,324 +262,238 @@ func GetTaxAmount(total int64, tax int) int64 {
return int64(float64(total) / float64((1 + taxD)))
}
-func (c *CartGrain) HandleMessage(message *Message, isReplay bool) (*FrameWithPayload, error) {
- if message.TimeStamp == nil {
- now := time.Now().Unix()
- message.TimeStamp = &now
- }
+func (c *CartGrain) Apply(content interface{}, isReplay bool) (*CartGrain, error) {
grainMutations.Inc()
- var err error
- switch message.Type {
- case SetCartItemsType:
- msg, ok := message.Content.(*messages.SetCartRequest)
- if !ok {
- err = fmt.Errorf("expected SetCartItems")
- } else {
- c.mu.Lock()
- c.Items = make([]*CartItem, 0, len(msg.Items))
- c.mu.Unlock()
- for _, item := range msg.Items {
- c.AddItem(item.Sku, int(item.Quantity), item.Country, item.StoreId)
- }
+ switch msg := content.(type) {
- }
- case AddRequestType:
- msg, ok := message.Content.(*messages.AddRequest)
- if !ok {
- err = fmt.Errorf("expected AddRequest")
- } else {
-
- existingItem, found := c.FindItemWithSku(msg.Sku)
- if found {
- existingItem.Quantity += int(msg.Quantity)
- c.UpdateTotals()
- } else {
- return c.AddItem(msg.Sku, int(msg.Quantity), msg.Country, msg.StoreId)
- }
-
- }
- case AddItemType:
- msg, ok := message.Content.(*messages.AddItem)
- if !ok {
- err = fmt.Errorf("expected AddItem")
- } else {
-
- if msg.Quantity < 1 {
- return nil, fmt.Errorf("invalid quantity")
- }
- existingItem, found := c.FindItemWithSku(msg.Sku)
- if found {
- existingItem.Quantity += int(msg.Quantity)
- c.UpdateTotals()
- } else {
- c.mu.Lock()
- c.lastItemId++
- tax := 2500
- if msg.Tax > 0 {
- tax = int(msg.Tax)
- }
-
- taxAmount := GetTaxAmount(msg.Price, tax)
-
- c.Items = append(c.Items, &CartItem{
- Id: c.lastItemId,
- ItemId: int(msg.ItemId),
- Quantity: int(msg.Quantity),
- Sku: msg.Sku,
- Name: msg.Name,
- Price: msg.Price,
- TotalPrice: msg.Price * int64(msg.Quantity),
- TotalTax: int64(taxAmount * int64(msg.Quantity)),
- Image: msg.Image,
- Stock: StockStatus(msg.Stock),
- Disclaimer: msg.Disclaimer,
- Brand: msg.Brand,
- Category: msg.Category,
- Category2: msg.Category2,
- Category3: msg.Category3,
- Category4: msg.Category4,
- Category5: msg.Category5,
- OrgPrice: msg.OrgPrice,
- ArticleType: msg.ArticleType,
- Outlet: msg.Outlet,
- SellerId: msg.SellerId,
- SellerName: msg.SellerName,
- Tax: int(taxAmount),
- TaxRate: tax,
- StoreId: msg.StoreId,
- })
- c.UpdateTotals()
- c.mu.Unlock()
- }
-
- }
- case ChangeQuantityType:
- msg, ok := message.Content.(*messages.ChangeQuantity)
- if !ok {
- err = fmt.Errorf("expected ChangeQuantity")
- } else {
-
- for i, item := range c.Items {
- if item.Id == int(msg.Id) {
- if msg.Quantity <= 0 {
- //c.TotalPrice -= item.Price * int64(item.Quantity)
- c.Items = append(c.Items[:i], c.Items[i+1:]...)
- } else {
- //diff := int(msg.Quantity) - item.Quantity
- item.Quantity = int(msg.Quantity)
- //c.TotalPrice += item.Price * int64(diff)
- }
-
- break
- }
- }
- c.UpdateTotals()
-
- }
- case RemoveItemType:
- msg, ok := message.Content.(*messages.RemoveItem)
- if !ok {
- err = fmt.Errorf("expected RemoveItem")
- } else {
-
- items := make([]*CartItem, 0, len(c.Items))
- for _, item := range c.Items {
- if item.Id == int(msg.Id) {
- //c.TotalPrice -= item.Price * int64(item.Quantity)
- } else {
- items = append(items, item)
- }
- }
- c.Items = items
- c.UpdateTotals()
- }
- case SetDeliveryType:
- msg, ok := message.Content.(*messages.SetDelivery)
- if !ok {
- err = fmt.Errorf("expected SetDelivery")
- } else {
-
- c.lastDeliveryId++
- items := make([]int, 0)
- withDelivery := c.ItemsWithDelivery()
- if len(msg.Items) == 0 {
- items = append(items, c.ItemsWithoutDelivery()...)
- } else {
- for _, id := range msg.Items {
- for _, item := range c.Items {
- if item.Id == int(id) {
- if slices.Contains(withDelivery, item.Id) {
- return nil, fmt.Errorf("item already has delivery")
- }
- items = append(items, int(item.Id))
- break
- }
- }
- }
- }
- if len(items) > 0 {
- c.Deliveries = append(c.Deliveries, &CartDelivery{
- Id: c.lastDeliveryId,
- Provider: msg.Provider,
- PickupPoint: msg.PickupPoint,
- Price: 4900,
- Items: items,
- })
-
- c.UpdateTotals()
-
- }
-
- }
- case RemoveDeliveryType:
- msg, ok := message.Content.(*messages.RemoveDelivery)
- if !ok {
- err = fmt.Errorf("expected RemoveDelivery")
- } else {
-
- deliveries := make([]*CartDelivery, 0, len(c.Deliveries))
- for _, delivery := range c.Deliveries {
- if delivery.Id == int(msg.Id) {
- c.TotalPrice -= delivery.Price
- } else {
- deliveries = append(deliveries, delivery)
- }
- }
- c.Deliveries = deliveries
- c.UpdateTotals()
- }
- case SetPickupPointType:
- msg, ok := message.Content.(*messages.SetPickupPoint)
- if !ok {
- err = fmt.Errorf("expected SetPickupPoint")
- } else {
-
- for _, delivery := range c.Deliveries {
- if delivery.Id == int(msg.DeliveryId) {
- delivery.PickupPoint = &messages.PickupPoint{
- Id: msg.Id,
- Address: msg.Address,
- City: msg.City,
- Zip: msg.Zip,
- Country: msg.Country,
- Name: msg.Name,
- }
- break
- }
- }
-
- }
- case CreateCheckoutOrderType:
- msg, ok := message.Content.(*messages.CreateCheckoutOrder)
- if !ok {
- err = fmt.Errorf("expected CreateCheckoutOrder")
- } else {
-
- orderLines := make([]*Line, 0, len(c.Items))
-
- c.PaymentInProgress = true
- c.Processing = true
- for _, item := range c.Items {
-
- orderLines = append(orderLines, &Line{
- Type: "physical",
- Reference: item.Sku,
- Name: item.Name,
- Quantity: item.Quantity,
- UnitPrice: int(item.Price),
- TaxRate: 2500, // item.TaxRate,
- QuantityUnit: "st",
- TotalAmount: int(item.TotalPrice),
- TotalTaxAmount: int(item.TotalTax),
- ImageURL: fmt.Sprintf("https://www.elgiganten.se%s", item.Image),
- })
- }
- for _, line := range c.Deliveries {
- if line.Price > 0 {
- orderLines = append(orderLines, &Line{
- Type: "shipping_fee",
- Reference: line.Provider,
- Name: "Delivery",
- Quantity: 1,
- UnitPrice: int(line.Price),
- TaxRate: 2500, // item.TaxRate,
- QuantityUnit: "st",
- TotalAmount: int(line.Price),
- TotalTaxAmount: int(GetTaxAmount(line.Price, 2500)),
- })
- }
- }
- order := CheckoutOrder{
- PurchaseCountry: "SE",
- PurchaseCurrency: "SEK",
- Locale: "sv-se",
- OrderAmount: int(c.TotalPrice),
- OrderTaxAmount: int(c.TotalTax),
- OrderLines: orderLines,
- MerchantReference1: c.Id.String(),
- MerchantURLS: &CheckoutMerchantURLS{
- Terms: msg.Terms,
- Checkout: msg.Checkout,
- Confirmation: msg.Confirmation,
- Validation: msg.Validation,
- Push: msg.Push,
- },
- }
- orderPayload, err := json.Marshal(order)
- if err != nil {
- return nil, err
- }
- var klarnaOrder *CheckoutOrder
- if c.OrderReference != "" {
- log.Printf("Updating order id %s", c.OrderReference)
- klarnaOrder, err = KlarnaInstance.UpdateOrder(c.OrderReference, bytes.NewReader(orderPayload))
- } else {
- klarnaOrder, err = KlarnaInstance.CreateOrder(bytes.NewReader(orderPayload))
- }
- // log.Printf("Order result: %+v", klarnaOrder)
- if nil != err {
- log.Printf("error from klarna: %v", err)
- return nil, err
- }
- if c.OrderReference == "" {
- c.OrderReference = klarnaOrder.ID
- c.PaymentStatus = klarnaOrder.Status
- }
-
- orderData, err := json.Marshal(klarnaOrder)
- if nil != err {
- return nil, err
- }
-
- result := MakeFrameWithPayload(RemoteCreateOrderReply, 200, orderData)
- return &result, nil
- }
- case OrderCompletedType:
- msg, ok := message.Content.(*messages.OrderCreated)
- if !ok {
- log.Printf("expected OrderCompleted, got %T", message.Content)
- err = fmt.Errorf("expected OrderCompleted")
- } else {
- c.OrderReference = msg.OrderId
- c.PaymentStatus = msg.Status
- c.PaymentInProgress = false
- }
- default:
- err = fmt.Errorf("unknown message type %d", message.Type)
- }
- if err != nil {
- return nil, err
- }
-
- if !isReplay {
+ case *messages.SetCartRequest:
c.mu.Lock()
- c.storageMessages = append(c.storageMessages, *message)
+ c.Items = make([]*CartItem, 0, len(msg.Items))
c.mu.Unlock()
+ for _, it := range msg.Items {
+ c.AddItem(it.Sku, int(it.Quantity), it.Country, it.StoreId)
+ }
+
+ case *messages.AddRequest:
+ if existing, found := c.FindItemWithSku(msg.Sku); found {
+ existing.Quantity += int(msg.Quantity)
+ c.UpdateTotals()
+ } else {
+ return c.AddItem(msg.Sku, int(msg.Quantity), msg.Country, msg.StoreId)
+ }
+
+ case *messages.AddItem:
+ if msg.Quantity < 1 {
+ return nil, fmt.Errorf("invalid quantity")
+ }
+ if existing, found := c.FindItemWithSku(msg.Sku); found {
+ existing.Quantity += int(msg.Quantity)
+ c.UpdateTotals()
+ } else {
+ c.mu.Lock()
+ c.lastItemId++
+ tax := 2500
+ if msg.Tax > 0 {
+ tax = int(msg.Tax)
+ }
+ taxAmount := GetTaxAmount(msg.Price, tax)
+ c.Items = append(c.Items, &CartItem{
+ Id: c.lastItemId,
+ ItemId: int(msg.ItemId),
+ Quantity: int(msg.Quantity),
+ Sku: msg.Sku,
+ Name: msg.Name,
+ Price: msg.Price,
+ TotalPrice: msg.Price * int64(msg.Quantity),
+ TotalTax: int64(taxAmount * int64(msg.Quantity)),
+ Image: msg.Image,
+ Stock: StockStatus(msg.Stock),
+ Disclaimer: msg.Disclaimer,
+ Brand: msg.Brand,
+ Category: msg.Category,
+ Category2: msg.Category2,
+ Category3: msg.Category3,
+ Category4: msg.Category4,
+ Category5: msg.Category5,
+ OrgPrice: msg.OrgPrice,
+ ArticleType: msg.ArticleType,
+ Outlet: msg.Outlet,
+ SellerId: msg.SellerId,
+ SellerName: msg.SellerName,
+ Tax: int(taxAmount),
+ TaxRate: tax,
+ StoreId: msg.StoreId,
+ })
+ c.UpdateTotals()
+ c.mu.Unlock()
+ }
+
+ case *messages.ChangeQuantity:
+ for i, item := range c.Items {
+ if item.Id == int(msg.Id) {
+ if msg.Quantity <= 0 {
+ c.Items = append(c.Items[:i], c.Items[i+1:]...)
+ } else {
+ item.Quantity = int(msg.Quantity)
+ }
+ break
+ }
+ }
+ c.UpdateTotals()
+
+ case *messages.RemoveItem:
+ newItems := make([]*CartItem, 0, len(c.Items))
+ for _, it := range c.Items {
+ if it.Id != int(msg.Id) {
+ newItems = append(newItems, it)
+ }
+ }
+ c.Items = newItems
+ c.UpdateTotals()
+
+ case *messages.SetDelivery:
+ c.lastDeliveryId++
+ items := make([]int, 0)
+ withDelivery := c.ItemsWithDelivery()
+ if len(msg.Items) == 0 {
+ items = append(items, c.ItemsWithoutDelivery()...)
+ } else {
+ for _, id := range msg.Items {
+ for _, it := range c.Items {
+ if it.Id == int(id) {
+ if slices.Contains(withDelivery, it.Id) {
+ return nil, fmt.Errorf("item already has delivery")
+ }
+ items = append(items, int(it.Id))
+ break
+ }
+ }
+ }
+ }
+ if len(items) > 0 {
+ c.Deliveries = append(c.Deliveries, &CartDelivery{
+ Id: c.lastDeliveryId,
+ Provider: msg.Provider,
+ PickupPoint: msg.PickupPoint,
+ Price: 4900,
+ Items: items,
+ })
+ c.UpdateTotals()
+ }
+
+ case *messages.RemoveDelivery:
+ dels := make([]*CartDelivery, 0, len(c.Deliveries))
+ for _, d := range c.Deliveries {
+ if d.Id == int(msg.Id) {
+ c.TotalPrice -= d.Price
+ } else {
+ dels = append(dels, d)
+ }
+ }
+ c.Deliveries = dels
+ c.UpdateTotals()
+
+ case *messages.SetPickupPoint:
+ for _, d := range c.Deliveries {
+ if d.Id == int(msg.DeliveryId) {
+ d.PickupPoint = &messages.PickupPoint{
+ Id: msg.Id,
+ Address: msg.Address,
+ City: msg.City,
+ Zip: msg.Zip,
+ Country: msg.Country,
+ Name: msg.Name,
+ }
+ break
+ }
+ }
+
+ case *messages.CreateCheckoutOrder:
+ orderLines := make([]*Line, 0, len(c.Items))
+ c.PaymentInProgress = true
+ c.Processing = true
+ for _, item := range c.Items {
+ orderLines = append(orderLines, &Line{
+ Type: "physical",
+ Reference: item.Sku,
+ Name: item.Name,
+ Quantity: item.Quantity,
+ UnitPrice: int(item.Price),
+ TaxRate: 2500,
+ QuantityUnit: "st",
+ TotalAmount: int(item.TotalPrice),
+ TotalTaxAmount: int(item.TotalTax),
+ ImageURL: fmt.Sprintf("https://www.elgiganten.se%s", item.Image),
+ })
+ }
+ for _, line := range c.Deliveries {
+ if line.Price > 0 {
+ orderLines = append(orderLines, &Line{
+ Type: "shipping_fee",
+ Reference: line.Provider,
+ Name: "Delivery",
+ Quantity: 1,
+ UnitPrice: int(line.Price),
+ TaxRate: 2500,
+ QuantityUnit: "st",
+ TotalAmount: int(line.Price),
+ TotalTaxAmount: int(GetTaxAmount(line.Price, 2500)),
+ })
+ }
+ }
+ order := CheckoutOrder{
+ PurchaseCountry: "SE",
+ PurchaseCurrency: "SEK",
+ Locale: "sv-se",
+ OrderAmount: int(c.TotalPrice),
+ OrderTaxAmount: int(c.TotalTax),
+ OrderLines: orderLines,
+ MerchantReference1: c.Id.String(),
+ MerchantURLS: &CheckoutMerchantURLS{
+ Terms: msg.Terms,
+ Checkout: msg.Checkout,
+ Confirmation: msg.Confirmation,
+ Validation: msg.Validation,
+ Push: msg.Push,
+ },
+ }
+ orderPayload, err := json.Marshal(order)
+ if err != nil {
+ return nil, err
+ }
+ var klarnaOrder *CheckoutOrder
+ var klarnaError error
+ if c.OrderReference != "" {
+ log.Printf("Updating order id %s", c.OrderReference)
+ klarnaOrder, klarnaError = KlarnaInstance.UpdateOrder(c.OrderReference, bytes.NewReader(orderPayload))
+ } else {
+ klarnaOrder, klarnaError = KlarnaInstance.CreateOrder(bytes.NewReader(orderPayload))
+ }
+ if klarnaError != nil {
+ log.Printf("error from klarna: %v", klarnaError)
+ return nil, klarnaError
+ }
+ if c.OrderReference == "" {
+ c.OrderReference = klarnaOrder.ID
+ c.PaymentStatus = klarnaOrder.Status
+ }
+ // This originally returned a FrameWithPayload; now returns the grain state.
+ // The caller (gRPC handler) is responsible for wrapping this.
+ return c, nil
+
+ case *messages.OrderCreated:
+ c.OrderReference = msg.OrderId
+ c.PaymentStatus = msg.Status
+ c.PaymentInProgress = false
+
+ default:
+ return nil, fmt.Errorf("unsupported mutation type %T", content)
}
- result, err := json.Marshal(c)
- msg := MakeFrameWithPayload(RemoteHandleMutationReply, 200, result)
- return &msg, err
+
+ // (Optional) Append to new storage mechanism here if still required.
+
+ return c, nil
}
func (c *CartGrain) UpdateTotals() {
diff --git a/cart_state_mapper.go b/cart_state_mapper.go
new file mode 100644
index 0000000..7a76c12
--- /dev/null
+++ b/cart_state_mapper.go
@@ -0,0 +1,211 @@
+package main
+
+import (
+ messages "git.tornberg.me/go-cart-actor/proto"
+)
+
+// cart_state_mapper.go
+//
+// Utilities to translate between internal CartGrain state and the gRPC
+// (typed) protobuf representation CartState. This replaces the previous
+// JSON blob framing and enables type-safe replies over gRPC, as well as
+// internal reuse for HTTP handlers without an extra marshal / unmarshal
+// hop (you can marshal CartState directly for JSON responses if desired).
+//
+// Only the one‑way mapping (CartGrain -> CartState) is strictly required
+// for mutation / state replies. A reverse helper is included in case
+// future features (e.g. snapshot import, replay, or migration) need it.
+
+// ToCartState converts the in‑memory CartGrain into a protobuf CartState.
+func ToCartState(c *CartGrain) *messages.CartState {
+ if c == nil {
+ return nil
+ }
+
+ items := make([]*messages.CartItemState, 0, len(c.Items))
+ for _, it := range c.Items {
+ if it == nil {
+ continue
+ }
+ itemDiscountPerUnit := max(0, it.OrgPrice-it.Price)
+ itemTotalDiscount := itemDiscountPerUnit * int64(it.Quantity)
+
+ items = append(items, &messages.CartItemState{
+ Id: int64(it.Id),
+ SourceItemId: int64(it.ItemId),
+ Sku: it.Sku,
+ Name: it.Name,
+ UnitPrice: it.Price,
+ Quantity: int32(it.Quantity),
+ TotalPrice: it.TotalPrice,
+ TotalTax: it.TotalTax,
+ OrgPrice: it.OrgPrice,
+ TaxRate: int32(it.TaxRate),
+ TotalDiscount: itemTotalDiscount,
+ Brand: it.Brand,
+ Category: it.Category,
+ Category2: it.Category2,
+ Category3: it.Category3,
+ Category4: it.Category4,
+ Category5: it.Category5,
+ Image: it.Image,
+ ArticleType: it.ArticleType,
+ SellerId: it.SellerId,
+ SellerName: it.SellerName,
+ Disclaimer: it.Disclaimer,
+ Outlet: deref(it.Outlet),
+ StoreId: deref(it.StoreId),
+ Stock: int32(it.Stock),
+ })
+ }
+
+ deliveries := make([]*messages.DeliveryState, 0, len(c.Deliveries))
+ for _, d := range c.Deliveries {
+ if d == nil {
+ continue
+ }
+ itemIds := make([]int64, 0, len(d.Items))
+ for _, id := range d.Items {
+ itemIds = append(itemIds, int64(id))
+ }
+ var pp *messages.PickupPoint
+ if d.PickupPoint != nil {
+ // Copy to avoid accidental shared mutation (proto points are fine but explicit).
+ pp = &messages.PickupPoint{
+ Id: d.PickupPoint.Id,
+ Name: d.PickupPoint.Name,
+ Address: d.PickupPoint.Address,
+ City: d.PickupPoint.City,
+ Zip: d.PickupPoint.Zip,
+ Country: d.PickupPoint.Country,
+ }
+ }
+ deliveries = append(deliveries, &messages.DeliveryState{
+ Id: int64(d.Id),
+ Provider: d.Provider,
+ Price: d.Price,
+ ItemIds: itemIds,
+ PickupPoint: pp,
+ })
+ }
+
+ return &messages.CartState{
+ CartId: c.Id.String(),
+ Items: items,
+ TotalPrice: c.TotalPrice,
+ TotalTax: c.TotalTax,
+ TotalDiscount: c.TotalDiscount,
+ Deliveries: deliveries,
+ PaymentInProgress: c.PaymentInProgress,
+ OrderReference: c.OrderReference,
+ PaymentStatus: c.PaymentStatus,
+ }
+}
+
+// FromCartState merges a protobuf CartState into an existing CartGrain.
+// This is optional and primarily useful for snapshot import or testing.
+func FromCartState(cs *messages.CartState, g *CartGrain) *CartGrain {
+ if cs == nil {
+ return g
+ }
+ if g == nil {
+ g = &CartGrain{}
+ }
+ g.Id = ToCartId(cs.CartId)
+ g.TotalPrice = cs.TotalPrice
+ g.TotalTax = cs.TotalTax
+ g.TotalDiscount = cs.TotalDiscount
+ g.PaymentInProgress = cs.PaymentInProgress
+ g.OrderReference = cs.OrderReference
+ g.PaymentStatus = cs.PaymentStatus
+
+ // Items
+ g.Items = g.Items[:0]
+ for _, it := range cs.Items {
+ if it == nil {
+ continue
+ }
+ outlet := toPtr(it.Outlet)
+ storeId := toPtr(it.StoreId)
+ g.Items = append(g.Items, &CartItem{
+ Id: int(it.Id),
+ ItemId: int(it.SourceItemId),
+ Sku: it.Sku,
+ Name: it.Name,
+ Price: it.UnitPrice,
+ Quantity: int(it.Quantity),
+ TotalPrice: it.TotalPrice,
+ TotalTax: it.TotalTax,
+ OrgPrice: it.OrgPrice,
+ TaxRate: int(it.TaxRate),
+ Brand: it.Brand,
+ Category: it.Category,
+ Category2: it.Category2,
+ Category3: it.Category3,
+ Category4: it.Category4,
+ Category5: it.Category5,
+ Image: it.Image,
+ ArticleType: it.ArticleType,
+ SellerId: it.SellerId,
+ SellerName: it.SellerName,
+ Disclaimer: it.Disclaimer,
+ Outlet: outlet,
+ StoreId: storeId,
+ Stock: StockStatus(it.Stock),
+ // Tax, TaxRate already set via Price / Totals if needed
+ })
+ if it.Id > int64(g.lastItemId) {
+ g.lastItemId = int(it.Id)
+ }
+ }
+
+ // Deliveries
+ g.Deliveries = g.Deliveries[:0]
+ for _, d := range cs.Deliveries {
+ if d == nil {
+ continue
+ }
+ intIds := make([]int, 0, len(d.ItemIds))
+ for _, id := range d.ItemIds {
+ intIds = append(intIds, int(id))
+ }
+ var pp *messages.PickupPoint
+ if d.PickupPoint != nil {
+ pp = &messages.PickupPoint{
+ Id: d.PickupPoint.Id,
+ Name: d.PickupPoint.Name,
+ Address: d.PickupPoint.Address,
+ City: d.PickupPoint.City,
+ Zip: d.PickupPoint.Zip,
+ Country: d.PickupPoint.Country,
+ }
+ }
+ g.Deliveries = append(g.Deliveries, &CartDelivery{
+ Id: int(d.Id),
+ Provider: d.Provider,
+ Price: d.Price,
+ Items: intIds,
+ PickupPoint: pp,
+ })
+ if d.Id > int64(g.lastDeliveryId) {
+ g.lastDeliveryId = int(d.Id)
+ }
+ }
+
+ return g
+}
+
+// Helper to safely de-reference optional string pointers to value or "".
+func deref(p *string) string {
+ if p == nil {
+ return ""
+ }
+ return *p
+}
+
+func toPtr(s string) *string {
+ if s == "" {
+ return nil
+ }
+ return &s
+}
diff --git a/disk-storage.go b/disk-storage.go
index e6876ce..452c9e3 100644
--- a/disk-storage.go
+++ b/disk-storage.go
@@ -3,7 +3,6 @@ package main
import (
"encoding/gob"
"fmt"
- "log"
"os"
"time"
)
@@ -23,59 +22,18 @@ func NewDiskStorage(stateFile string) (*DiskStorage, error) {
return ret, err
}
-func saveMessages(messages []StorableMessage, id CartId) error {
-
- if len(messages) == 0 {
- return nil
- }
- log.Printf("%d messages to save for grain id %s", len(messages), id)
- var file *os.File
- var err error
- path := getCartPath(id.String())
- file, err = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
- if err != nil {
- return err
- }
- defer file.Close()
-
- for _, m := range messages {
- err := m.Write(file)
- if err != nil {
- return err
- }
- }
- return err
+func saveMessages(_ interface{}, _ CartId) error {
+ // No-op: legacy event log persistence removed in oneof refactor.
+ return nil
}
func getCartPath(id string) string {
return fmt.Sprintf("data/%s.prot", id)
}
-func loadMessages(grain Grain, id CartId) error {
- var err error
- path := getCartPath(id.String())
-
- file, err := os.Open(path)
- if err != nil {
- if os.IsNotExist(err) {
- return nil
- }
- return err
- }
- defer file.Close()
-
- for err == nil {
- var msg Message
- err = ReadMessage(file, &msg)
- if err == nil {
- grain.HandleMessage(&msg, true)
- }
- }
-
- if err.Error() == "EOF" {
- return nil
- }
- return err
+func loadMessages(_ Grain, _ CartId) error {
+ // No-op: legacy replay removed in oneof refactor.
+ return nil
}
func (s *DiskStorage) saveState() error {
@@ -103,15 +61,8 @@ func (s *DiskStorage) loadState() error {
return gob.NewDecoder(file).Decode(&s.LastSaves)
}
-func (s *DiskStorage) Store(id CartId, grain *CartGrain) error {
- lastSavedMessage, ok := s.LastSaves[id]
- if ok && lastSavedMessage > grain.GetLastChange() {
- return nil
- }
- err := saveMessages(grain.GetStorageMessage(lastSavedMessage), id)
- if err != nil {
- return err
- }
+func (s *DiskStorage) Store(id CartId, _ *CartGrain) error {
+ // With the removal of the legacy message log, we only update the timestamp.
ts := time.Now().Unix()
s.LastSaves[id] = ts
s.lastSave = ts
diff --git a/go.mod b/go.mod
index 0148cea..3baefef 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,6 @@ require (
github.com/google/uuid v1.6.0
github.com/matst80/slask-finder v0.0.0-20251009175145-ce05aff5a548
github.com/prometheus/client_golang v1.23.2
- github.com/rabbitmq/amqp091-go v1.10.0
google.golang.org/grpc v1.76.0
google.golang.org/protobuf v1.36.10
k8s.io/api v0.34.1
diff --git a/go.sum b/go.sum
index 7c2ab17..ef3aab2 100644
--- a/go.sum
+++ b/go.sum
@@ -70,14 +70,10 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
-github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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/matst80/slask-finder v0.0.0-20251009175145-ce05aff5a548 h1:lkxVz5lNPlU78El49cx1c3Rxo/bp7Gbzp2BjSRFgg1U=
github.com/matst80/slask-finder v0.0.0-20251009175145-ce05aff5a548/go.mod h1:k4lo5gFYb3AqrgftlraCnv95xvjB/w8udRqJQJ12mAE=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -104,8 +100,6 @@ github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oE
github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
-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/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
diff --git a/grain-pool.go b/grain-pool.go
index f69a8fa..8391948 100644
--- a/grain-pool.go
+++ b/grain-pool.go
@@ -1,7 +1,6 @@
package main
import (
- "encoding/json"
"fmt"
"log"
"sync"
@@ -27,8 +26,8 @@ var (
)
type GrainPool interface {
- Process(id CartId, messages ...Message) (*FrameWithPayload, error)
- Get(id CartId) (*FrameWithPayload, error)
+ Process(id CartId, mutations ...interface{}) (*CartGrain, error)
+ Get(id CartId) (*CartGrain, error)
}
type Ttl struct {
@@ -143,26 +142,20 @@ func (p *GrainLocalPool) GetGrain(id CartId) (*CartGrain, error) {
return grain, err
}
-func (p *GrainLocalPool) Process(id CartId, messages ...Message) (*FrameWithPayload, error) {
+func (p *GrainLocalPool) Process(id CartId, mutations ...interface{}) (*CartGrain, error) {
grain, err := p.GetGrain(id)
- var result *FrameWithPayload
+ var result *CartGrain
if err == nil && grain != nil {
- for _, message := range messages {
- result, err = grain.HandleMessage(&message, false)
+ for _, m := range mutations {
+ result, err = grain.Apply(m, false)
+ if err != nil {
+ break
+ }
}
}
return result, err
}
-func (p *GrainLocalPool) Get(id CartId) (*FrameWithPayload, error) {
- grain, err := p.GetGrain(id)
- if err != nil {
- return nil, err
- }
- data, err := json.Marshal(grain)
- if err != nil {
- return nil, err
- }
- ret := MakeFrameWithPayload(0, 200, data)
- return &ret, nil
+func (p *GrainLocalPool) Get(id CartId) (*CartGrain, error) {
+ return p.GetGrain(id)
}
diff --git a/grpc_integration_test.go b/grpc_integration_test.go
index 201c034..3e54581 100644
--- a/grpc_integration_test.go
+++ b/grpc_integration_test.go
@@ -1,9 +1,7 @@
package main
import (
- "bytes"
"context"
- "encoding/json"
"fmt"
"testing"
"time"
@@ -14,7 +12,7 @@ import (
// TestCartActorMutationAndState validates end-to-end gRPC mutation + state retrieval
// against a locally started gRPC server (single-node scenario).
-// This test uses AddItemType directly to avoid hitting external product
+// This test uses the oneof MutationEnvelope directly to avoid hitting external product
// fetching logic (FetchItem) which would require network I/O.
func TestCartActorMutationAndState(t *testing.T) {
// Setup local grain pool + synced pool (no discovery, single host)
@@ -62,35 +60,30 @@ func TestCartActorMutationAndState(t *testing.T) {
Country: "se",
}
- // Marshal underlying mutation payload using the existing handler code path
- handler, ok := Handlers[AddItemType]
- if !ok {
- t.Fatalf("Handler for AddItemType missing")
- }
- payloadData, err := getSerializedPayload(handler, AddItemType, addItem)
- if err != nil {
- t.Fatalf("serialize add item: %v", err)
+ // Build oneof envelope directly (no legacy handler/enum)
+ envelope := &messages.MutationEnvelope{
+ CartId: cartID,
+ ClientTimestamp: time.Now().Unix(),
+ Mutation: &messages.MutationEnvelope_AddItem{
+ AddItem: addItem,
+ },
}
// Issue Mutate RPC
- mutResp, err := cartClient.Mutate(context.Background(), &messages.MutationRequest{
- CartId: cartID,
- Type: messages.MutationType(AddItemType),
- Payload: payloadData,
- ClientTimestamp: time.Now().Unix(),
- })
+ mutResp, err := cartClient.Mutate(context.Background(), envelope)
if err != nil {
t.Fatalf("Mutate RPC error: %v", err)
}
if mutResp.StatusCode != 200 {
- t.Fatalf("Mutate returned non-200 status: %d payload=%s", mutResp.StatusCode, string(mutResp.Payload))
+ t.Fatalf("Mutate returned non-200 status: %d, error: %s", mutResp.StatusCode, mutResp.GetError())
}
- // Decode cart state JSON and validate
- state := &CartGrain{}
- if err := json.Unmarshal(mutResp.Payload, state); err != nil {
- t.Fatalf("Unmarshal mutate cart state: %v\nPayload: %s", err, string(mutResp.Payload))
+ // Validate the response state
+ state := mutResp.GetState()
+ if state == nil {
+ t.Fatalf("Mutate response state is nil")
}
+
if len(state.Items) != 1 {
t.Fatalf("Expected 1 item after mutation, got %d", len(state.Items))
}
@@ -106,13 +99,14 @@ func TestCartActorMutationAndState(t *testing.T) {
t.Fatalf("GetState RPC error: %v", err)
}
if getResp.StatusCode != 200 {
- t.Fatalf("GetState returned non-200 status: %d payload=%s", getResp.StatusCode, string(getResp.Payload))
+ t.Fatalf("GetState returned non-200 status: %d, error: %s", getResp.StatusCode, getResp.GetError())
}
- state2 := &CartGrain{}
- if err := json.Unmarshal(getResp.Payload, state2); err != nil {
- t.Fatalf("Unmarshal get state: %v", err)
+ state2 := getResp.GetState()
+ if state2 == nil {
+ t.Fatalf("GetState response state is nil")
}
+
if len(state2.Items) != 1 {
t.Fatalf("Expected 1 item in GetState, got %d", len(state2.Items))
}
@@ -121,15 +115,4 @@ func TestCartActorMutationAndState(t *testing.T) {
}
}
-// getSerializedPayload serializes a mutation proto using the registered handler.
-func getSerializedPayload(handler MessageHandler, msgType uint16, content interface{}) ([]byte, error) {
- msg := &Message{
- Type: msgType,
- Content: content,
- }
- var buf bytes.Buffer
- if err := handler.Write(msg, &buf); err != nil {
- return nil, err
- }
- return buf.Bytes(), nil
-}
+// Legacy serialization helper removed (oneof envelope used directly)
diff --git a/grpc_server.go b/grpc_server.go
index 1b10422..c5b2a42 100644
--- a/grpc_server.go
+++ b/grpc_server.go
@@ -2,378 +2,138 @@ package main
import (
"context"
- "errors"
"fmt"
"log"
"net"
- "time"
- proto "git.tornberg.me/go-cart-actor/proto" // underlying generated package name is 'messages'
- "github.com/prometheus/client_golang/prometheus"
- "github.com/prometheus/client_golang/prometheus/promauto"
+ messages "git.tornberg.me/go-cart-actor/proto"
"google.golang.org/grpc"
- "google.golang.org/grpc/codes"
- "google.golang.org/grpc/status"
+ "google.golang.org/grpc/reflection"
)
-// -----------------------------------------------------------------------------
-// Metrics
-// -----------------------------------------------------------------------------
+// cartActorGRPCServer implements the CartActor and ControlPlane gRPC services.
+// It delegates cart operations to a grain pool and cluster operations to a synced pool.
+type cartActorGRPCServer struct {
+ messages.UnimplementedCartActorServer
+ messages.UnimplementedControlPlaneServer
-var (
- grpcMutateDuration = promauto.NewHistogram(prometheus.HistogramOpts{
- Name: "cart_grpc_mutate_duration_seconds",
- Help: "Duration of CartActor.Mutate RPCs",
- Buckets: prometheus.DefBuckets,
- })
- grpcMutateErrors = promauto.NewCounter(prometheus.CounterOpts{
- Name: "cart_grpc_mutate_errors_total",
- Help: "Total number of failed CartActor.Mutate RPCs",
- })
- grpcStateDuration = promauto.NewHistogram(prometheus.HistogramOpts{
- Name: "cart_grpc_get_state_duration_seconds",
- Help: "Duration of CartActor.GetState RPCs",
- Buckets: prometheus.DefBuckets,
- })
- grpcControlDuration = promauto.NewHistogram(prometheus.HistogramOpts{
- Name: "cart_grpc_control_duration_seconds",
- Help: "Duration of ControlPlane RPCs",
- Buckets: prometheus.DefBuckets,
- })
- grpcControlErrors = promauto.NewCounter(prometheus.CounterOpts{
- Name: "cart_grpc_control_errors_total",
- Help: "Total number of failed ControlPlane RPCs",
- })
-)
-
-// timeTrack wraps a closure and records duration into the supplied histogram.
-func timeTrack(hist prometheus.Observer, fn func() error) (err error) {
- start := time.Now()
- defer func() {
- hist.Observe(time.Since(start).Seconds())
- }()
- return fn()
+ pool GrainPool // For cart state mutations and queries
+ syncedPool *SyncedPool // For cluster membership and control
}
-// -----------------------------------------------------------------------------
-// CartActor Service Implementation
-// -----------------------------------------------------------------------------
-
-type cartActorService struct {
- proto.UnimplementedCartActorServer
- pool GrainPool
+// NewCartActorGRPCServer creates and initializes the server.
+func NewCartActorGRPCServer(pool GrainPool, syncedPool *SyncedPool) *cartActorGRPCServer {
+ return &cartActorGRPCServer{
+ pool: pool,
+ syncedPool: syncedPool,
+ }
}
-func newCartActorService(pool GrainPool) *cartActorService {
- return &cartActorService{pool: pool}
-}
+// Mutate applies a mutation from an envelope to the corresponding cart grain.
+func (s *cartActorGRPCServer) Mutate(ctx context.Context, envelope *messages.MutationEnvelope) (*messages.MutationReply, error) {
+ if envelope.GetCartId() == "" {
+ return &messages.MutationReply{
+ StatusCode: 400,
+ Result: &messages.MutationReply_Error{Error: "cart_id is required"},
+ }, nil
+ }
+ cartID := ToCartId(envelope.GetCartId())
-func (s *cartActorService) Mutate(ctx context.Context, req *proto.MutationRequest) (*proto.MutationReply, error) {
- var reply *proto.MutationReply
- err := timeTrack(grpcMutateDuration, func() error {
- if req == nil {
- return status.Error(codes.InvalidArgument, "request is nil")
- }
- if req.CartId == "" {
- return status.Error(codes.InvalidArgument, "cart_id is empty")
- }
- mt := uint16(req.Type.Number())
- handler, ok := Handlers[mt]
- if !ok {
- return status.Errorf(codes.InvalidArgument, "unknown mutation type %d", mt)
- }
- content, err := handler.Read(req.Payload)
- if err != nil {
- return status.Errorf(codes.InvalidArgument, "decode payload: %v", err)
- }
+ var mutation interface{}
+ switch m := envelope.Mutation.(type) {
+ case *messages.MutationEnvelope_AddRequest:
+ mutation = m.AddRequest
+ case *messages.MutationEnvelope_AddItem:
+ mutation = m.AddItem
+ case *messages.MutationEnvelope_RemoveItem:
+ mutation = m.RemoveItem
+ case *messages.MutationEnvelope_RemoveDelivery:
+ mutation = m.RemoveDelivery
+ case *messages.MutationEnvelope_ChangeQuantity:
+ mutation = m.ChangeQuantity
+ case *messages.MutationEnvelope_SetDelivery:
+ mutation = m.SetDelivery
+ case *messages.MutationEnvelope_SetPickupPoint:
+ mutation = m.SetPickupPoint
+ case *messages.MutationEnvelope_CreateCheckoutOrder:
+ mutation = m.CreateCheckoutOrder
+ case *messages.MutationEnvelope_SetCartItems:
+ mutation = m.SetCartItems
+ case *messages.MutationEnvelope_OrderCompleted:
+ mutation = m.OrderCompleted
+ default:
+ return &messages.MutationReply{
+ StatusCode: 400,
+ Result: &messages.MutationReply_Error{Error: fmt.Sprintf("unsupported mutation type: %T", m)},
+ }, nil
+ }
- ts := req.ClientTimestamp
- if ts == 0 {
- ts = time.Now().Unix()
- }
- msg := Message{
- Type: mt,
- TimeStamp: &ts,
- Content: content,
- }
-
- frame, err := s.pool.Process(ToCartId(req.CartId), msg)
- if err != nil {
- return err
- }
- reply = &proto.MutationReply{
- StatusCode: int32(frame.StatusCode),
- Payload: frame.Payload,
- }
- return nil
- })
+ // Delegate the mutation to the grain pool.
+ // The pool is responsible for routing it to the correct grain (local or remote).
+ grain, err := s.pool.Process(cartID, mutation)
if err != nil {
- grpcMutateErrors.Inc()
- return nil, err
- }
- return reply, nil
-}
-
-func (s *cartActorService) GetState(ctx context.Context, req *proto.StateRequest) (*proto.StateReply, error) {
- var reply *proto.StateReply
- err := timeTrack(grpcStateDuration, func() error {
- if req == nil || req.CartId == "" {
- return status.Error(codes.InvalidArgument, "cart_id is empty")
- }
- frame, err := s.pool.Get(ToCartId(req.CartId))
- if err != nil {
- return err
- }
- reply = &proto.StateReply{
- StatusCode: int32(frame.StatusCode),
- Payload: frame.Payload,
- }
- return nil
- })
- if err != nil {
- return nil, err
- }
- return reply, nil
-}
-
-// -----------------------------------------------------------------------------
-// ControlPlane Service Implementation
-// -----------------------------------------------------------------------------
-
-// controlPlaneService directly leverages SyncedPool internals (same package).
-// NOTE: This is a transitional adapter; once the legacy frame-based code is
-// removed, related fields/methods in SyncedPool can be slimmed.
-type controlPlaneService struct {
- proto.UnimplementedControlPlaneServer
- pool *SyncedPool
-}
-
-func newControlPlaneService(pool *SyncedPool) *controlPlaneService {
- return &controlPlaneService{pool: pool}
-}
-
-func (s *controlPlaneService) Ping(ctx context.Context, _ *proto.Empty) (*proto.PingReply, error) {
- var reply *proto.PingReply
- err := timeTrack(grpcControlDuration, func() error {
- reply = &proto.PingReply{
- Host: s.pool.Hostname,
- UnixTime: time.Now().Unix(),
- }
- return nil
- })
- if err != nil {
- grpcControlErrors.Inc()
- return nil, err
- }
- return reply, nil
-}
-
-func (s *controlPlaneService) Negotiate(ctx context.Context, req *proto.NegotiateRequest) (*proto.NegotiateReply, error) {
- var reply *proto.NegotiateReply
- err := timeTrack(grpcControlDuration, func() error {
- if req == nil {
- return status.Error(codes.InvalidArgument, "request is nil")
- }
- // Add unknown hosts
- for _, host := range req.KnownHosts {
- if host == "" || host == s.pool.Hostname {
- continue
- }
- if !s.pool.IsKnown(host) {
- go s.pool.AddRemote(host)
- }
- }
- // Build healthy host list
- hosts := make([]string, 0)
- for _, r := range s.pool.GetHealthyRemotes() {
- hosts = append(hosts, r.Host)
- }
- hosts = append(hosts, s.pool.Hostname)
- reply = &proto.NegotiateReply{
- Hosts: hosts,
- }
- return nil
- })
- if err != nil {
- grpcControlErrors.Inc()
- return nil, err
- }
- return reply, nil
-}
-
-func (s *controlPlaneService) GetCartIds(ctx context.Context, _ *proto.Empty) (*proto.CartIdsReply, error) {
- var reply *proto.CartIdsReply
- err := timeTrack(grpcControlDuration, func() error {
- s.pool.mu.RLock()
- defer s.pool.mu.RUnlock()
- ids := make([]string, 0, len(s.pool.local.grains))
- for id, g := range s.pool.local.grains {
- if g == nil {
- continue
- }
- if id.String() == "" {
- continue
- }
- ids = append(ids, id.String())
- }
- reply = &proto.CartIdsReply{
- CartIds: ids,
- }
- return nil
- })
- if err != nil {
- grpcControlErrors.Inc()
- return nil, err
- }
- return reply, nil
-}
-
-func (s *controlPlaneService) ConfirmOwner(ctx context.Context, req *proto.OwnerChangeRequest) (*proto.OwnerChangeAck, error) {
- var reply *proto.OwnerChangeAck
- err := timeTrack(grpcControlDuration, func() error {
- if req == nil || req.CartId == "" || req.NewHost == "" {
- return status.Error(codes.InvalidArgument, "cart_id or new_host missing")
- }
- id := ToCartId(req.CartId)
- newHost := req.NewHost
-
- // Mirror GrainOwnerChangeHandler semantics
- log.Printf("gRPC ConfirmOwner: cart %s newHost=%s", id, newHost)
- for _, r := range s.pool.remoteHosts {
- if r.Host == newHost && r.IsHealthy() {
- go s.pool.SpawnRemoteGrain(id, newHost)
- break
- }
- }
- go s.pool.AddRemote(newHost)
-
- reply = &proto.OwnerChangeAck{
- Accepted: true,
- Message: "ok",
- }
- return nil
- })
- if err != nil {
- grpcControlErrors.Inc()
- return nil, err
- }
- return reply, nil
-}
-
-func (s *controlPlaneService) Closing(ctx context.Context, notice *proto.ClosingNotice) (*proto.OwnerChangeAck, error) {
- var reply *proto.OwnerChangeAck
- err := timeTrack(grpcControlDuration, func() error {
- if notice == nil || notice.Host == "" {
- return status.Error(codes.InvalidArgument, "host missing")
- }
- host := notice.Host
- s.pool.mu.RLock()
- _, exists := s.pool.remoteHosts[host]
- s.pool.mu.RUnlock()
- if exists {
- go s.pool.RemoveHost(host)
- }
- reply = &proto.OwnerChangeAck{
- Accepted: true,
- Message: "removed",
- }
- return nil
- })
- if err != nil {
- grpcControlErrors.Inc()
- return nil, err
- }
- return reply, nil
-}
-
-// -----------------------------------------------------------------------------
-// Server Bootstrap
-// -----------------------------------------------------------------------------
-
-type GRPCServer struct {
- server *grpc.Server
- lis net.Listener
- addr string
-}
-
-// StartGRPCServer sets up a gRPC server hosting both CartActor and ControlPlane services.
-// addr example: ":1337" (for combined) OR run two servers if you want separate ports.
-// For the migration we can host both on the same listener to reduce open ports.
-func StartGRPCServer(addr string, pool GrainPool, synced *SyncedPool, opts ...grpc.ServerOption) (*GRPCServer, error) {
- if pool == nil {
- return nil, errors.New("nil grain pool")
- }
- if synced == nil {
- return nil, errors.New("nil synced pool")
+ return &messages.MutationReply{
+ StatusCode: 500,
+ Result: &messages.MutationReply_Error{Error: err.Error()},
+ }, nil
}
- lis, err := net.Listen("tcp", addr)
- if err != nil {
- return nil, fmt.Errorf("listen %s: %w", addr, err)
- }
+ // Map the internal grain state to the protobuf representation.
+ cartState := ToCartState(grain)
- grpcServer := grpc.NewServer(opts...)
- proto.RegisterCartActorServer(grpcServer, newCartActorService(pool))
- proto.RegisterControlPlaneServer(grpcServer, newControlPlaneService(synced))
-
- go func() {
- log.Printf("gRPC server listening on %s", addr)
- if serveErr := grpcServer.Serve(lis); serveErr != nil {
- log.Printf("gRPC server stopped: %v", serveErr)
- }
- }()
-
- return &GRPCServer{
- server: grpcServer,
- lis: lis,
- addr: addr,
+ return &messages.MutationReply{
+ StatusCode: 200,
+ Result: &messages.MutationReply_State{State: cartState},
}, nil
}
-// GracefulStop stops the server gracefully.
-func (s *GRPCServer) GracefulStop() {
- if s == nil || s.server == nil {
- return
+// GetState retrieves the current state of a cart grain.
+func (s *cartActorGRPCServer) GetState(ctx context.Context, req *messages.StateRequest) (*messages.StateReply, error) {
+ if req.GetCartId() == "" {
+ return &messages.StateReply{
+ StatusCode: 400,
+ Result: &messages.StateReply_Error{Error: "cart_id is required"},
+ }, nil
}
- s.server.GracefulStop()
-}
+ cartID := ToCartId(req.GetCartId())
-// Addr returns the bound address.
-func (s *GRPCServer) Addr() string {
- if s == nil {
- return ""
- }
- return s.addr
-}
-
-// -----------------------------------------------------------------------------
-// Client Dial Helpers (used later by refactored remote grain + control plane)
-// -----------------------------------------------------------------------------
-
-// DialRemote establishes (or reuses externally) a gRPC client connection.
-func DialRemote(ctx context.Context, target string, opts ...grpc.DialOption) (*grpc.ClientConn, error) {
- dialOpts := []grpc.DialOption{
- grpc.WithInsecure(), // NOTE: Intentional for initial migration; replace with TLS / mTLS later.
- grpc.WithBlock(),
- }
- dialOpts = append(dialOpts, opts...)
- ctxDial, cancel := context.WithTimeout(ctx, 5*time.Second)
- defer cancel()
- conn, err := grpc.DialContext(ctxDial, target, dialOpts...)
+ grain, err := s.pool.Get(cartID)
if err != nil {
- return nil, err
+ return &messages.StateReply{
+ StatusCode: 500,
+ Result: &messages.StateReply_Error{Error: err.Error()},
+ }, nil
}
- return conn, nil
+
+ cartState := ToCartState(grain)
+
+ return &messages.StateReply{
+ StatusCode: 200,
+ Result: &messages.StateReply_State{State: cartState},
+ }, nil
}
-// -----------------------------------------------------------------------------
-// Utility for converting internal errors to gRPC status (if needed later).
-// -----------------------------------------------------------------------------
-
-func grpcError(err error) error {
- if err == nil {
- return nil
+// StartGRPCServer configures and starts the unified gRPC server on the given address.
+// It registers both the CartActor and ControlPlane services.
+func StartGRPCServer(addr string, pool GrainPool, syncedPool *SyncedPool) (*grpc.Server, error) {
+ lis, err := net.Listen("tcp", addr)
+ if err != nil {
+ return nil, fmt.Errorf("failed to listen: %w", err)
}
- // Extend mapping if we add richer error types.
- return status.Error(codes.Internal, err.Error())
-}
+
+ grpcServer := grpc.NewServer()
+ server := NewCartActorGRPCServer(pool, syncedPool)
+
+ messages.RegisterCartActorServer(grpcServer, server)
+ messages.RegisterControlPlaneServer(grpcServer, server)
+ reflection.Register(grpcServer)
+
+ log.Printf("gRPC server listening on %s", addr)
+ go func() {
+ if err := grpcServer.Serve(lis); err != nil {
+ log.Fatalf("failed to serve gRPC: %v", err)
+ }
+ }()
+
+ return grpcServer, nil
+}
\ No newline at end of file
diff --git a/main.go b/main.go
index 417c3ea..1e32e7c 100644
--- a/main.go
+++ b/main.go
@@ -1,21 +1,15 @@
package main
import (
- "encoding/json"
"fmt"
"log"
- "net/http"
- "net/http/pprof"
"os"
"os/signal"
- "strings"
"syscall"
"time"
- messages "git.tornberg.me/go-cart-actor/proto"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
- "github.com/prometheus/client_golang/prometheus/promhttp"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
@@ -38,13 +32,13 @@ var (
func spawn(id CartId) (*CartGrain, error) {
grainSpawns.Inc()
ret := &CartGrain{
- lastItemId: 0,
- lastDeliveryId: 0,
- Deliveries: []*CartDelivery{},
- Id: id,
- Items: []*CartItem{},
- storageMessages: []Message{},
- TotalPrice: 0,
+ lastItemId: 0,
+ lastDeliveryId: 0,
+ Deliveries: []*CartDelivery{},
+ Id: id,
+ Items: []*CartItem{},
+ // storageMessages removed (legacy event log deprecated)
+ TotalPrice: 0,
}
err := loadMessages(ret, id)
return ret, err
@@ -82,15 +76,7 @@ func (a *App) Save() error {
return a.storage.saveState()
}
-func (a *App) HandleSave(w http.ResponseWriter, r *http.Request) {
- err := a.Save()
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- w.Write([]byte(err.Error()))
- } else {
- w.WriteHeader(http.StatusCreated)
- }
-}
+
var podIp = os.Getenv("POD_IP")
var name = os.Getenv("POD_NAME")
@@ -114,44 +100,7 @@ func GetDiscovery() Discovery {
return NewK8sDiscovery(client)
}
-var tpl = `
-
-
-
-
- s10r testing - checkout
-
-
- %s
-
-
-`
-
-func getCountryFromHost(host string) string {
- if strings.Contains(strings.ToLower(host), "-no") {
- return "no"
- }
- return "se"
-}
-
-func getCheckoutOrder(host string, cartId CartId) *messages.CreateCheckoutOrder {
- baseUrl := fmt.Sprintf("https://%s", host)
- cartBaseUrl := os.Getenv("CART_BASE_URL")
- if cartBaseUrl == "" {
- cartBaseUrl = "https://cart.tornberg.me"
- }
- country := getCountryFromHost(host)
-
- return &messages.CreateCheckoutOrder{
- Terms: fmt.Sprintf("%s/terms", baseUrl),
- Checkout: fmt.Sprintf("%s/checkout?order_id={checkout.order.id}", baseUrl),
- Confirmation: fmt.Sprintf("%s/confirmation/{checkout.order.id}", baseUrl),
- Validation: fmt.Sprintf("%s/validation", cartBaseUrl),
- Push: fmt.Sprintf("%s/push?order_id={checkout.order.id}", cartBaseUrl),
- Country: country,
- }
-}
func main() {
@@ -185,193 +134,7 @@ func main() {
}
}
}()
- orderHandler := &AmqpOrderHandler{
- Url: amqpUrl,
- }
- syncedServer := NewPoolServer(syncedPool, fmt.Sprintf("%s, %s", name, podIp))
- mux := http.NewServeMux()
- mux.Handle("/cart/", http.StripPrefix("/cart", syncedServer.Serve()))
- // only for local
- // mux.HandleFunc("GET /add/remote/{host}", func(w http.ResponseWriter, r *http.Request) {
- // syncedPool.AddRemote(r.PathValue("host"))
- // })
- // mux.HandleFunc("GET /save", app.HandleSave)
- //mux.HandleFunc("/", app.RewritePath)
- mux.HandleFunc("/debug/pprof/", pprof.Index)
- mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
- mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
- mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
- mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
- mux.Handle("/metrics", promhttp.Handler())
- mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
- // Grain pool health: simple capacity check (mirrors previous GrainHandler.IsHealthy)
- app.pool.mu.RLock()
- grainCount := len(app.pool.grains)
- capacity := app.pool.PoolSize
- app.pool.mu.RUnlock()
- if grainCount >= capacity {
- w.WriteHeader(http.StatusInternalServerError)
- w.Write([]byte("grain pool at capacity"))
- return
- }
- if !syncedPool.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("/checkout", func(w http.ResponseWriter, r *http.Request) {
- orderId := r.URL.Query().Get("order_id")
- order := &CheckoutOrder{}
-
- if orderId == "" {
- cookie, err := r.Cookie("cartid")
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
- if cookie.Value == "" {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte("no cart id to checkout is empty"))
- return
- }
- cartId := ToCartId(cookie.Value)
- reply, err := syncedServer.pool.Process(cartId, Message{
- Type: CreateCheckoutOrderType,
- Content: getCheckoutOrder(r.Host, cartId),
- })
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- w.Write([]byte(err.Error()))
- }
- err = json.Unmarshal(reply.Payload, &order)
- if err != nil {
- w.WriteHeader(http.StatusInternalServerError)
- w.Write([]byte(err.Error()))
- return
- }
- } else {
- prevOrder, err := KlarnaInstance.GetOrder(orderId)
- if err != nil {
- w.WriteHeader(http.StatusBadRequest)
- w.Write([]byte(err.Error()))
- return
- }
- order = prevOrder
- }
- 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)
- w.Write([]byte(fmt.Sprintf(tpl, order.HTMLSnippet)))
- })
- mux.HandleFunc("/confirmation/{order_id}", func(w http.ResponseWriter, r *http.Request) {
-
- orderId := r.PathValue("order_id")
- order, err := KlarnaInstance.GetOrder(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)
- w.Write([]byte(fmt.Sprintf(tpl, order.HTMLSnippet)))
- })
- mux.HandleFunc("/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)
- }
- log.Printf("Klarna order validation: %s", order.ID)
- //err = confirmOrder(order, orderHandler)
- //if err != nil {
- // log.Printf("Error validating order: %v\n", err)
- // w.WriteHeader(http.StatusInternalServerError)
- // return
- //}
- //
- //err = triggerOrderCompleted(err, syncedServer, order)
- //if err != nil {
- // log.Printf("Error processing cart message: %v\n", err)
- // w.WriteHeader(http.StatusInternalServerError)
- // return
- //}
- w.WriteHeader(http.StatusOK)
- })
- mux.HandleFunc("/push", func(w http.ResponseWriter, r *http.Request) {
-
- 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 := KlarnaInstance.GetOrder(orderId)
-
- if err != nil {
- log.Printf("Error creating request: %v\n", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- err = confirmOrder(order, orderHandler)
- if err != nil {
- log.Printf("Error confirming order: %v\n", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
-
- err = triggerOrderCompleted(err, syncedServer, order)
- if err != nil {
- log.Printf("Error processing cart message: %v\n", err)
- w.WriteHeader(http.StatusInternalServerError)
- return
- }
- err = KlarnaInstance.AcknowledgeOrder(orderId)
- if err != nil {
- log.Printf("Error acknowledging order: %v\n", err)
- }
-
- w.WriteHeader(http.StatusOK)
- })
- mux.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusOK)
- w.Write([]byte("1.0.0"))
- })
sigs := make(chan os.Signal, 1)
done := make(chan bool, 1)
@@ -385,37 +148,7 @@ func main() {
done <- true
}()
- log.Print("Server started at port 8080")
- go http.ListenAndServe(":8080", mux)
+ log.Print("Server started at port 1337")
<-done
}
-
-func triggerOrderCompleted(err error, syncedServer *PoolServer, order *CheckoutOrder) error {
- _, err = syncedServer.pool.Process(ToCartId(order.MerchantReference1), Message{
- Type: OrderCompletedType,
- Content: &messages.OrderCreated{
- OrderId: order.ID,
- Status: order.Status,
- },
- })
- return err
-}
-
-func confirmOrder(order *CheckoutOrder, orderHandler *AmqpOrderHandler) error {
- orderToSend, err := json.Marshal(order)
- if err != nil {
- return err
- }
- err = orderHandler.Connect()
- if err != nil {
- return err
- }
- defer orderHandler.Close()
- err = orderHandler.OrderCompleted(orderToSend)
- if err != nil {
- return err
- }
-
- return nil
-}
diff --git a/message-handler.go b/message-handler.go
deleted file mode 100644
index 36ab61c..0000000
--- a/message-handler.go
+++ /dev/null
@@ -1,315 +0,0 @@
-package main
-
-import (
- "fmt"
- "io"
-
- messages "git.tornberg.me/go-cart-actor/proto"
- "google.golang.org/protobuf/proto"
-)
-
-var Handlers = map[uint16]MessageHandler{
- AddRequestType: &AddRequestHandler{},
- AddItemType: &AddItemHandler{},
- ChangeQuantityType: &ChangeQuantityHandler{},
- SetDeliveryType: &SetDeliveryHandler{},
- RemoveItemType: &RemoveItemHandler{},
- RemoveDeliveryType: &RemoveDeliveryHandler{},
- CreateCheckoutOrderType: &CheckoutHandler{},
- SetCartItemsType: &SetCartItemsHandler{},
- OrderCompletedType: &OrderCompletedHandler{},
-}
-
-func GetMessageHandler(t uint16) (MessageHandler, error) {
- h, ok := Handlers[t]
- if !ok {
- return nil, fmt.Errorf("no handler for message type %d", t)
- }
- return h, nil
-}
-
-type MessageHandler interface {
- Write(*Message, io.Writer) error
- Read(data []byte) (interface{}, error)
- Is(*Message) bool
-}
-type TypedMessageHandler struct {
- Type uint16
-}
-
-type SetCartItemsHandler struct {
- TypedMessageHandler
-}
-
-func (h *SetCartItemsHandler) Write(m *Message, w io.Writer) error {
- messageBytes, err := proto.Marshal(m.Content.(*messages.SetCartRequest))
- if err != nil {
- return err
- }
- w.Write(messageBytes)
- return nil
-}
-
-func (h *SetCartItemsHandler) Read(data []byte) (interface{}, error) {
- msg := &messages.SetCartRequest{}
-
- err := proto.Unmarshal(data, msg)
- if err != nil {
- return nil, err
- }
- return msg, nil
-}
-
-func (h *SetCartItemsHandler) Is(m *Message) bool {
- if m.Type != AddRequestType {
- return false
- }
- _, ok := m.Content.(*messages.SetCartRequest)
- return ok
-}
-
-type AddRequestHandler struct {
- TypedMessageHandler
-}
-
-func (h *AddRequestHandler) Write(m *Message, w io.Writer) error {
- messageBytes, err := proto.Marshal(m.Content.(*messages.AddRequest))
- if err != nil {
- return err
- }
- w.Write(messageBytes)
- return nil
-}
-
-func (h *AddRequestHandler) Read(data []byte) (interface{}, error) {
- msg := &messages.AddRequest{}
-
- err := proto.Unmarshal(data, msg)
- if err != nil {
- return nil, err
- }
- return msg, nil
-}
-
-func (h *AddRequestHandler) Is(m *Message) bool {
- if m.Type != AddRequestType {
- return false
- }
- _, ok := m.Content.(*messages.AddRequest)
- return ok
-}
-
-type AddItemHandler struct {
- TypedMessageHandler
-}
-
-func (h *AddItemHandler) Write(m *Message, w io.Writer) error {
- messageBytes, err := proto.Marshal(m.Content.(*messages.AddItem))
- if err != nil {
- return err
- }
- w.Write(messageBytes)
- return nil
-}
-
-func (h *AddItemHandler) Read(data []byte) (interface{}, error) {
- msg := &messages.AddItem{}
-
- err := proto.Unmarshal(data, msg)
- if err != nil {
- return nil, err
- }
- return msg, nil
-}
-
-func (h *AddItemHandler) Is(m *Message) bool {
- if m.Type != AddItemType {
- return false
- }
- _, ok := m.Content.(*messages.AddItem)
- return ok
-}
-
-type ChangeQuantityHandler struct {
- TypedMessageHandler
-}
-
-func (h *ChangeQuantityHandler) Write(m *Message, w io.Writer) error {
- messageBytes, err := proto.Marshal(m.Content.(*messages.ChangeQuantity))
- if err != nil {
- return err
- }
- w.Write(messageBytes)
- return nil
-}
-
-func (h *ChangeQuantityHandler) Read(data []byte) (interface{}, error) {
- msg := &messages.ChangeQuantity{}
-
- err := proto.Unmarshal(data, msg)
- if err != nil {
- return nil, err
- }
- return msg, nil
-}
-
-func (h *ChangeQuantityHandler) Is(m *Message) bool {
- if m.Type != ChangeQuantityType {
- return false
- }
- _, ok := m.Content.(*messages.ChangeQuantity)
- return ok
-}
-
-type SetDeliveryHandler struct {
- TypedMessageHandler
-}
-
-func (h *SetDeliveryHandler) Write(m *Message, w io.Writer) error {
- messageBytes, err := proto.Marshal(m.Content.(*messages.SetDelivery))
- if err != nil {
- return err
- }
- w.Write(messageBytes)
- return nil
-}
-
-func (h *SetDeliveryHandler) Read(data []byte) (interface{}, error) {
- msg := &messages.SetDelivery{}
-
- err := proto.Unmarshal(data, msg)
- if err != nil {
- return nil, err
- }
- return msg, nil
-}
-
-func (h *SetDeliveryHandler) Is(m *Message) bool {
- if m.Type != ChangeQuantityType {
- return false
- }
- _, ok := m.Content.(*messages.SetDelivery)
- return ok
-}
-
-type RemoveItemHandler struct {
- TypedMessageHandler
-}
-
-func (h *RemoveItemHandler) Write(m *Message, w io.Writer) error {
- messageBytes, err := proto.Marshal(m.Content.(*messages.RemoveItem))
- if err != nil {
- return err
- }
- w.Write(messageBytes)
- return nil
-}
-
-func (h *RemoveItemHandler) Read(data []byte) (interface{}, error) {
- msg := &messages.RemoveItem{}
-
- err := proto.Unmarshal(data, msg)
- if err != nil {
- return nil, err
- }
- return msg, nil
-}
-
-func (h *RemoveItemHandler) Is(m *Message) bool {
- if m.Type != AddItemType {
- return false
- }
- _, ok := m.Content.(*messages.RemoveItem)
- return ok
-}
-
-type RemoveDeliveryHandler struct {
- TypedMessageHandler
-}
-
-func (h *RemoveDeliveryHandler) Write(m *Message, w io.Writer) error {
- messageBytes, err := proto.Marshal(m.Content.(*messages.RemoveDelivery))
- if err != nil {
- return err
- }
- w.Write(messageBytes)
- return nil
-}
-
-func (h *RemoveDeliveryHandler) Read(data []byte) (interface{}, error) {
- msg := &messages.RemoveDelivery{}
-
- err := proto.Unmarshal(data, msg)
- if err != nil {
- return nil, err
- }
- return msg, nil
-}
-
-func (h *RemoveDeliveryHandler) Is(m *Message) bool {
- if m.Type != AddItemType {
- return false
- }
- _, ok := m.Content.(*messages.RemoveDelivery)
- return ok
-}
-
-type CheckoutHandler struct {
- TypedMessageHandler
-}
-
-func (h *CheckoutHandler) Write(m *Message, w io.Writer) error {
- messageBytes, err := proto.Marshal(m.Content.(*messages.CreateCheckoutOrder))
- if err != nil {
- return err
- }
- w.Write(messageBytes)
- return nil
-}
-
-func (h *CheckoutHandler) Read(data []byte) (interface{}, error) {
- msg := &messages.CreateCheckoutOrder{}
-
- err := proto.Unmarshal(data, msg)
- if err != nil {
- return nil, err
- }
- return msg, nil
-}
-
-func (h *CheckoutHandler) Is(m *Message) bool {
- if m.Type != CreateCheckoutOrderType {
- return false
- }
- _, ok := m.Content.(*messages.CreateCheckoutOrder)
- return ok
-}
-
-type OrderCompletedHandler struct {
- TypedMessageHandler
-}
-
-func (h *OrderCompletedHandler) Write(m *Message, w io.Writer) error {
- messageBytes, err := proto.Marshal(m.Content.(*messages.OrderCreated))
- if err != nil {
- return err
- }
- w.Write(messageBytes)
- return nil
-}
-func (h *OrderCompletedHandler) Read(data []byte) (interface{}, error) {
- msg := &messages.OrderCreated{}
-
- err := proto.Unmarshal(data, msg)
- if err != nil {
- return nil, err
- }
- return msg, nil
-}
-func (h *OrderCompletedHandler) Is(m *Message) bool {
- if m.Type != OrderCompletedType {
- return false
- }
- _, ok := m.Content.(*messages.OrderCreated)
- return ok
-}
diff --git a/message-types.go b/message-types.go
deleted file mode 100644
index 28b11fc..0000000
--- a/message-types.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package main
-
-const (
- AddRequestType = 1
- AddItemType = 2
-
- RemoveItemType = 4
- RemoveDeliveryType = 5
- ChangeQuantityType = 6
- SetDeliveryType = 7
- SetPickupPointType = 8
- CreateCheckoutOrderType = 9
- SetCartItemsType = 10
- OrderCompletedType = 11
-)
diff --git a/message.go b/message.go
deleted file mode 100644
index 748a205..0000000
--- a/message.go
+++ /dev/null
@@ -1,94 +0,0 @@
-package main
-
-import (
- "bytes"
- "encoding/binary"
- "io"
- "time"
-)
-
-type StorableMessage interface {
- Write(w io.Writer) error
-}
-
-type Message struct {
- Type uint16
- TimeStamp *int64
- Content interface{}
-}
-
-type MessageWriter struct {
- io.Writer
-}
-
-type StorableMessageHeader struct {
- Version uint16
- Type uint16
- TimeStamp int64
- DataLength uint64
-}
-
-func GetData(fn func(w io.Writer) error) ([]byte, error) {
- var buf bytes.Buffer
- err := fn(&buf)
- if err != nil {
- return nil, err
- }
- b := buf.Bytes()
- return b, nil
-}
-
-func (m Message) Write(w io.Writer) error {
- h, err := GetMessageHandler(m.Type)
- if err != nil {
- return err
- }
- data, err := GetData(func(w io.Writer) error {
- return h.Write(&m, w)
- })
- if err != nil {
- return err
- }
- ts := time.Now().Unix()
- if m.TimeStamp != nil {
- ts = *m.TimeStamp
- }
-
- err = binary.Write(w, binary.LittleEndian, StorableMessageHeader{
- Version: 1,
- Type: m.Type,
- TimeStamp: ts,
- DataLength: uint64(len(data)),
- })
- w.Write(data)
-
- return err
-}
-
-func ReadMessage(reader io.Reader, m *Message) error {
-
- header := StorableMessageHeader{}
- err := binary.Read(reader, binary.LittleEndian, &header)
- if err != nil {
- return err
- }
- messageBytes := make([]byte, header.DataLength)
- _, err = reader.Read(messageBytes)
- if err != nil {
- return err
- }
- h, err := GetMessageHandler(header.Type)
- if err != nil {
- return err
- }
- content, err := h.Read(messageBytes)
- if err != nil {
- return err
- }
- m.Content = content
-
- m.Type = header.Type
- m.TimeStamp = &header.TimeStamp
-
- return nil
-}
diff --git a/pool-server.go b/pool-server.go
deleted file mode 100644
index 26f77f4..0000000
--- a/pool-server.go
+++ /dev/null
@@ -1,349 +0,0 @@
-package main
-
-import (
- "encoding/json"
- "fmt"
- "log"
- "math/rand"
- "net/http"
- "strconv"
- "time"
-
- messages "git.tornberg.me/go-cart-actor/proto"
-)
-
-type PoolServer struct {
- pod_name string
- pool GrainPool
-}
-
-func NewPoolServer(pool GrainPool, pod_name string) *PoolServer {
- return &PoolServer{
- pod_name: pod_name,
- pool: pool,
- }
-}
-
-func (s *PoolServer) HandleGet(w http.ResponseWriter, r *http.Request, id CartId) error {
- data, err := s.pool.Get(id)
- if err != nil {
- return err
- }
-
- return s.WriteResult(w, data)
-}
-
-func (s *PoolServer) HandleAddSku(w http.ResponseWriter, r *http.Request, id CartId) error {
- sku := r.PathValue("sku")
- data, err := s.pool.Process(id, Message{
- Type: AddRequestType,
- Content: &messages.AddRequest{Sku: sku, Quantity: 1},
- })
- if err != nil {
- return err
- }
- return s.WriteResult(w, data)
-}
-
-func ErrorHandler(fn func(w http.ResponseWriter, r *http.Request) error) func(w http.ResponseWriter, r *http.Request) {
- return func(w http.ResponseWriter, r *http.Request) {
- err := fn(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) WriteResult(w http.ResponseWriter, result *FrameWithPayload) 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.StatusCode != 200 {
- log.Printf("Call error: %d\n", result.StatusCode)
- if result.StatusCode >= 200 && result.StatusCode < 600 {
- w.WriteHeader(int(result.StatusCode))
- } else {
- w.WriteHeader(http.StatusInternalServerError)
- }
- w.Write([]byte(result.Payload))
- return nil
- }
- w.WriteHeader(http.StatusOK)
- _, err := w.Write(result.Payload)
- return err
-}
-
-func (s *PoolServer) HandleDeleteItem(w http.ResponseWriter, r *http.Request, id CartId) error {
-
- itemIdString := r.PathValue("itemId")
- itemId, err := strconv.Atoi(itemIdString)
- if err != nil {
- return err
- }
- data, err := s.pool.Process(id, Message{
- Type: RemoveItemType,
- Content: &messages.RemoveItem{Id: int64(itemId)},
- })
- if err != nil {
- return err
- }
- return s.WriteResult(w, data)
-}
-
-type SetDelivery struct {
- Provider string `json:"provider"`
- Items []int64 `json:"items"`
- PickupPoint *messages.PickupPoint `json:"pickupPoint,omitempty"`
-}
-
-func (s *PoolServer) HandleSetDelivery(w http.ResponseWriter, r *http.Request, id CartId) error {
-
- delivery := SetDelivery{}
- err := json.NewDecoder(r.Body).Decode(&delivery)
- if err != nil {
- return err
- }
- data, err := s.pool.Process(id, Message{
- Type: SetDeliveryType,
- Content: &messages.SetDelivery{
- Provider: delivery.Provider,
- Items: delivery.Items,
- PickupPoint: delivery.PickupPoint,
- },
- })
- if err != nil {
- return err
- }
- return s.WriteResult(w, data)
-}
-
-func (s *PoolServer) HandleSetPickupPoint(w http.ResponseWriter, r *http.Request, id CartId) error {
-
- deliveryIdString := r.PathValue("deliveryId")
- deliveryId, err := strconv.Atoi(deliveryIdString)
- if err != nil {
- return err
- }
- pickupPoint := messages.PickupPoint{}
- err = json.NewDecoder(r.Body).Decode(&pickupPoint)
- if err != nil {
- return err
- }
- reply, err := s.pool.Process(id, Message{
- Type: SetPickupPointType,
- Content: &messages.SetPickupPoint{
- DeliveryId: int64(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) HandleRemoveDelivery(w http.ResponseWriter, r *http.Request, id CartId) error {
-
- deliveryIdString := r.PathValue("deliveryId")
- deliveryId, err := strconv.Atoi(deliveryIdString)
- if err != nil {
- return err
- }
- reply, err := s.pool.Process(id, Message{
- Type: RemoveDeliveryType,
- Content: &messages.RemoveDelivery{Id: int64(deliveryId)},
- })
- if err != nil {
- return err
- }
- return s.WriteResult(w, reply)
-}
-
-func (s *PoolServer) HandleQuantityChange(w http.ResponseWriter, r *http.Request, id CartId) error {
- changeQuantity := messages.ChangeQuantity{}
- err := json.NewDecoder(r.Body).Decode(&changeQuantity)
- if err != nil {
- return err
- }
- reply, err := s.pool.Process(id, Message{
- Type: ChangeQuantityType,
- Content: &changeQuantity,
- })
- if err != nil {
- return err
- }
- return s.WriteResult(w, reply)
-}
-
-func (s *PoolServer) HandleSetCartItems(w http.ResponseWriter, r *http.Request, id CartId) error {
- setCartItems := messages.SetCartRequest{}
- err := json.NewDecoder(r.Body).Decode(&setCartItems)
- if err != nil {
- return err
- }
- reply, err := s.pool.Process(id, Message{
- Type: SetCartItemsType,
- Content: &setCartItems,
- })
- if err != nil {
- return err
- }
- return s.WriteResult(w, reply)
-}
-
-func (s *PoolServer) HandleAddRequest(w http.ResponseWriter, r *http.Request, id CartId) error {
- addRequest := messages.AddRequest{}
- err := json.NewDecoder(r.Body).Decode(&addRequest)
- if err != nil {
- return err
- }
- reply, err := s.pool.Process(id, Message{
- Type: AddRequestType,
- Content: &addRequest,
- })
- if err != nil {
- return err
- }
- return s.WriteResult(w, reply)
-}
-
-func (s *PoolServer) HandleConfirmation(w http.ResponseWriter, r *http.Request, id CartId) error {
- orderId := r.PathValue("orderId")
- if orderId == "" {
- return fmt.Errorf("orderId is empty")
- }
- order, err := KlarnaInstance.GetOrder(orderId)
-
- if err != nil {
- return err
- }
-
- w.Header().Set("Content-Type", "application/json")
- w.Header().Set("X-Pod-Name", s.pod_name)
- w.Header().Set("Cache-Control", "no-cache")
- w.Header().Set("Access-Control-Allow-Origin", "*")
- w.WriteHeader(http.StatusOK)
- return json.NewEncoder(w).Encode(order)
-}
-
-func (s *PoolServer) HandleCheckout(w http.ResponseWriter, r *http.Request, id CartId) error {
-
- reply, err := s.pool.Process(id, Message{
- Type: CreateCheckoutOrderType,
- Content: &messages.CreateCheckoutOrder{
- Terms: "https://slask-finder.tornberg.me/terms",
- Checkout: "https://slask-finder.tornberg.me/checkout?order_id={checkout.order.id}",
- Confirmation: "https://slask-finder.tornberg.me/confirmation/{checkout.order.id}",
- Push: "https://cart.tornberg.me/push?order_id={checkout.order.id}",
- },
- })
- if err != nil {
- return err
- }
- if reply.StatusCode != 200 {
- return s.WriteResult(w, reply)
- }
-
- // w.Header().Set("Content-Type", "application/json")
- // w.Header().Set("X-Pod-Name", s.pod_name)
- // w.Header().Set("Cache-Control", "no-cache")
- // w.Header().Set("Access-Control-Allow-Origin", "*")
- // w.WriteHeader(http.StatusOK)
-
- return s.WriteResult(w, reply)
-}
-
-func NewCartId() CartId {
- id := time.Now().UnixNano() + rand.Int63()
-
- return ToCartId(fmt.Sprintf("%d", id))
-}
-
-func CookieCartIdHandler(fn func(w http.ResponseWriter, r *http.Request, cartId CartId) error) func(w http.ResponseWriter, r *http.Request) error {
- return func(w http.ResponseWriter, r *http.Request) error {
- var cartId CartId
- cartIdCookie := r.CookiesNamed("cartid")
- if cartIdCookie == nil || len(cartIdCookie) == 0 {
- cartId = NewCartId()
- http.SetCookie(w, &http.Cookie{
- Name: "cartid",
- Value: cartId.String(),
- Secure: true,
- HttpOnly: true,
- Path: "/",
- Expires: time.Now().AddDate(0, 0, 14),
- SameSite: http.SameSiteLaxMode,
- })
- } else {
- cartId = ToCartId(cartIdCookie[0].Value)
- }
- return fn(w, r, cartId)
- }
-}
-
-func (s *PoolServer) RemoveCartCookie(w http.ResponseWriter, r *http.Request, cartId CartId) error {
- cartId = NewCartId()
- http.SetCookie(w, &http.Cookie{
- Name: "cartid",
- Value: cartId.String(),
- Path: "/",
- Secure: true,
- HttpOnly: true,
- Expires: time.Unix(0, 0),
- SameSite: http.SameSiteLaxMode,
- })
- w.WriteHeader(http.StatusOK)
- return nil
-}
-
-func CartIdHandler(fn func(w http.ResponseWriter, r *http.Request, cartId CartId) error) func(w http.ResponseWriter, r *http.Request) error {
- return func(w http.ResponseWriter, r *http.Request) error {
- cartId := ToCartId(r.PathValue("id"))
- return fn(w, r, cartId)
- }
-}
-
-func (s *PoolServer) Serve() *http.ServeMux {
- mux := http.NewServeMux()
- //mux.HandleFunc("/", s.RewritePath)
- mux.HandleFunc("OPTIONS /", func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Access-Control-Allow-Origin", "*")
- w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE")
- w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
- w.WriteHeader(http.StatusOK)
- })
-
- mux.HandleFunc("GET /", ErrorHandler(CookieCartIdHandler(s.HandleGet)))
- mux.HandleFunc("GET /add/{sku}", ErrorHandler(CookieCartIdHandler(s.HandleAddSku)))
- mux.HandleFunc("POST /", ErrorHandler(CookieCartIdHandler(s.HandleAddRequest)))
- mux.HandleFunc("POST /set", ErrorHandler(CookieCartIdHandler(s.HandleSetCartItems)))
- mux.HandleFunc("DELETE /{itemId}", ErrorHandler(CookieCartIdHandler(s.HandleDeleteItem)))
- mux.HandleFunc("PUT /", ErrorHandler(CookieCartIdHandler(s.HandleQuantityChange)))
- mux.HandleFunc("DELETE /", ErrorHandler(CookieCartIdHandler(s.RemoveCartCookie)))
- mux.HandleFunc("POST /delivery", ErrorHandler(CookieCartIdHandler(s.HandleSetDelivery)))
- mux.HandleFunc("DELETE /delivery/{deliveryId}", ErrorHandler(CookieCartIdHandler(s.HandleRemoveDelivery)))
- mux.HandleFunc("PUT /delivery/{deliveryId}/pickupPoint", ErrorHandler(CookieCartIdHandler(s.HandleSetPickupPoint)))
- mux.HandleFunc("GET /checkout", ErrorHandler(CookieCartIdHandler(s.HandleCheckout)))
- mux.HandleFunc("GET /confirmation/{orderId}", ErrorHandler(CookieCartIdHandler(s.HandleConfirmation)))
-
- mux.HandleFunc("GET /byid/{id}", ErrorHandler(CartIdHandler(s.HandleGet)))
- mux.HandleFunc("GET /byid/{id}/add/{sku}", ErrorHandler(CartIdHandler(s.HandleAddSku)))
- mux.HandleFunc("POST /byid/{id}", ErrorHandler(CartIdHandler(s.HandleAddRequest)))
- mux.HandleFunc("DELETE /byid/{id}/{itemId}", ErrorHandler(CartIdHandler(s.HandleDeleteItem)))
- mux.HandleFunc("PUT /byid/{id}", ErrorHandler(CartIdHandler(s.HandleQuantityChange)))
- mux.HandleFunc("POST /byid/{id}/delivery", ErrorHandler(CartIdHandler(s.HandleSetDelivery)))
- mux.HandleFunc("DELETE /byid/{id}/delivery/{deliveryId}", ErrorHandler(CartIdHandler(s.HandleRemoveDelivery)))
- mux.HandleFunc("PUT /byid/{id}/delivery/{deliveryId}/pickupPoint", ErrorHandler(CartIdHandler(s.HandleSetPickupPoint)))
- mux.HandleFunc("GET /byid/{id}/checkout", ErrorHandler(CartIdHandler(s.HandleCheckout)))
- mux.HandleFunc("GET /byid/{id}/confirmation", ErrorHandler(CartIdHandler(s.HandleConfirmation)))
-
- return mux
-}
diff --git a/proto/cart_actor.pb.go b/proto/cart_actor.pb.go
index 7f57b85..f7f1a73 100644
--- a/proto/cart_actor.pb.go
+++ b/proto/cart_actor.pb.go
@@ -21,110 +21,45 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
-// MutationType corresponds 1:1 with the legacy uint16 message type constants.
-type MutationType int32
-
-const (
- MutationType_MUTATION_TYPE_UNSPECIFIED MutationType = 0
- MutationType_MUTATION_ADD_REQUEST MutationType = 1
- MutationType_MUTATION_ADD_ITEM MutationType = 2
- // (3 was unused / reserved in legacy framing)
- MutationType_MUTATION_REMOVE_ITEM MutationType = 4
- MutationType_MUTATION_REMOVE_DELIVERY MutationType = 5
- MutationType_MUTATION_CHANGE_QUANTITY MutationType = 6
- MutationType_MUTATION_SET_DELIVERY MutationType = 7
- MutationType_MUTATION_SET_PICKUP_POINT MutationType = 8
- MutationType_MUTATION_CREATE_CHECKOUT_ORDER MutationType = 9
- MutationType_MUTATION_SET_CART_ITEMS MutationType = 10
- MutationType_MUTATION_ORDER_COMPLETED MutationType = 11
-)
-
-// Enum value maps for MutationType.
-var (
- MutationType_name = map[int32]string{
- 0: "MUTATION_TYPE_UNSPECIFIED",
- 1: "MUTATION_ADD_REQUEST",
- 2: "MUTATION_ADD_ITEM",
- 4: "MUTATION_REMOVE_ITEM",
- 5: "MUTATION_REMOVE_DELIVERY",
- 6: "MUTATION_CHANGE_QUANTITY",
- 7: "MUTATION_SET_DELIVERY",
- 8: "MUTATION_SET_PICKUP_POINT",
- 9: "MUTATION_CREATE_CHECKOUT_ORDER",
- 10: "MUTATION_SET_CART_ITEMS",
- 11: "MUTATION_ORDER_COMPLETED",
- }
- MutationType_value = map[string]int32{
- "MUTATION_TYPE_UNSPECIFIED": 0,
- "MUTATION_ADD_REQUEST": 1,
- "MUTATION_ADD_ITEM": 2,
- "MUTATION_REMOVE_ITEM": 4,
- "MUTATION_REMOVE_DELIVERY": 5,
- "MUTATION_CHANGE_QUANTITY": 6,
- "MUTATION_SET_DELIVERY": 7,
- "MUTATION_SET_PICKUP_POINT": 8,
- "MUTATION_CREATE_CHECKOUT_ORDER": 9,
- "MUTATION_SET_CART_ITEMS": 10,
- "MUTATION_ORDER_COMPLETED": 11,
- }
-)
-
-func (x MutationType) Enum() *MutationType {
- p := new(MutationType)
- *p = x
- return p
-}
-
-func (x MutationType) String() string {
- return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
-}
-
-func (MutationType) Descriptor() protoreflect.EnumDescriptor {
- return file_cart_actor_proto_enumTypes[0].Descriptor()
-}
-
-func (MutationType) Type() protoreflect.EnumType {
- return &file_cart_actor_proto_enumTypes[0]
-}
-
-func (x MutationType) Number() protoreflect.EnumNumber {
- return protoreflect.EnumNumber(x)
-}
-
-// Deprecated: Use MutationType.Descriptor instead.
-func (MutationType) EnumDescriptor() ([]byte, []int) {
- return file_cart_actor_proto_rawDescGZIP(), []int{0}
-}
-
-// MutationRequest is an envelope:
-// - cart_id: string form of CartId (legacy 16-byte array truncated/padded).
-// - type: mutation kind (see enum).
-// - payload: serialized underlying proto message (AddRequest, AddItem, etc.).
-// - client_timestamp: optional unix timestamp; server sets if zero.
-type MutationRequest struct {
+// MutationEnvelope carries exactly one mutation plus metadata.
+// client_timestamp:
+// - Optional Unix timestamp provided by the client.
+// - If zero the server MAY overwrite with its local time.
+type MutationEnvelope struct {
state protoimpl.MessageState `protogen:"open.v1"`
CartId string `protobuf:"bytes,1,opt,name=cart_id,json=cartId,proto3" json:"cart_id,omitempty"`
- Type MutationType `protobuf:"varint,2,opt,name=type,proto3,enum=messages.MutationType" json:"type,omitempty"`
- Payload []byte `protobuf:"bytes,3,opt,name=payload,proto3" json:"payload,omitempty"`
- ClientTimestamp int64 `protobuf:"varint,4,opt,name=client_timestamp,json=clientTimestamp,proto3" json:"client_timestamp,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
+ ClientTimestamp int64 `protobuf:"varint,2,opt,name=client_timestamp,json=clientTimestamp,proto3" json:"client_timestamp,omitempty"`
+ // Types that are valid to be assigned to Mutation:
+ //
+ // *MutationEnvelope_AddRequest
+ // *MutationEnvelope_AddItem
+ // *MutationEnvelope_RemoveItem
+ // *MutationEnvelope_RemoveDelivery
+ // *MutationEnvelope_ChangeQuantity
+ // *MutationEnvelope_SetDelivery
+ // *MutationEnvelope_SetPickupPoint
+ // *MutationEnvelope_CreateCheckoutOrder
+ // *MutationEnvelope_SetCartItems
+ // *MutationEnvelope_OrderCompleted
+ Mutation isMutationEnvelope_Mutation `protobuf_oneof:"mutation"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
}
-func (x *MutationRequest) Reset() {
- *x = MutationRequest{}
+func (x *MutationEnvelope) Reset() {
+ *x = MutationEnvelope{}
mi := &file_cart_actor_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
-func (x *MutationRequest) String() string {
+func (x *MutationEnvelope) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*MutationRequest) ProtoMessage() {}
+func (*MutationEnvelope) ProtoMessage() {}
-func (x *MutationRequest) ProtoReflect() protoreflect.Message {
+func (x *MutationEnvelope) ProtoReflect() protoreflect.Message {
mi := &file_cart_actor_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -136,45 +71,198 @@ func (x *MutationRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use MutationRequest.ProtoReflect.Descriptor instead.
-func (*MutationRequest) Descriptor() ([]byte, []int) {
+// Deprecated: Use MutationEnvelope.ProtoReflect.Descriptor instead.
+func (*MutationEnvelope) Descriptor() ([]byte, []int) {
return file_cart_actor_proto_rawDescGZIP(), []int{0}
}
-func (x *MutationRequest) GetCartId() string {
+func (x *MutationEnvelope) GetCartId() string {
if x != nil {
return x.CartId
}
return ""
}
-func (x *MutationRequest) GetType() MutationType {
- if x != nil {
- return x.Type
- }
- return MutationType_MUTATION_TYPE_UNSPECIFIED
-}
-
-func (x *MutationRequest) GetPayload() []byte {
- if x != nil {
- return x.Payload
- }
- return nil
-}
-
-func (x *MutationRequest) GetClientTimestamp() int64 {
+func (x *MutationEnvelope) GetClientTimestamp() int64 {
if x != nil {
return x.ClientTimestamp
}
return 0
}
-// MutationReply returns a status code (legacy semantics) plus a JSON payload
-// representing the full cart state (or an error message if non-200).
+func (x *MutationEnvelope) GetMutation() isMutationEnvelope_Mutation {
+ if x != nil {
+ return x.Mutation
+ }
+ return nil
+}
+
+func (x *MutationEnvelope) GetAddRequest() *AddRequest {
+ if x != nil {
+ if x, ok := x.Mutation.(*MutationEnvelope_AddRequest); ok {
+ return x.AddRequest
+ }
+ }
+ return nil
+}
+
+func (x *MutationEnvelope) GetAddItem() *AddItem {
+ if x != nil {
+ if x, ok := x.Mutation.(*MutationEnvelope_AddItem); ok {
+ return x.AddItem
+ }
+ }
+ return nil
+}
+
+func (x *MutationEnvelope) GetRemoveItem() *RemoveItem {
+ if x != nil {
+ if x, ok := x.Mutation.(*MutationEnvelope_RemoveItem); ok {
+ return x.RemoveItem
+ }
+ }
+ return nil
+}
+
+func (x *MutationEnvelope) GetRemoveDelivery() *RemoveDelivery {
+ if x != nil {
+ if x, ok := x.Mutation.(*MutationEnvelope_RemoveDelivery); ok {
+ return x.RemoveDelivery
+ }
+ }
+ return nil
+}
+
+func (x *MutationEnvelope) GetChangeQuantity() *ChangeQuantity {
+ if x != nil {
+ if x, ok := x.Mutation.(*MutationEnvelope_ChangeQuantity); ok {
+ return x.ChangeQuantity
+ }
+ }
+ return nil
+}
+
+func (x *MutationEnvelope) GetSetDelivery() *SetDelivery {
+ if x != nil {
+ if x, ok := x.Mutation.(*MutationEnvelope_SetDelivery); ok {
+ return x.SetDelivery
+ }
+ }
+ return nil
+}
+
+func (x *MutationEnvelope) GetSetPickupPoint() *SetPickupPoint {
+ if x != nil {
+ if x, ok := x.Mutation.(*MutationEnvelope_SetPickupPoint); ok {
+ return x.SetPickupPoint
+ }
+ }
+ return nil
+}
+
+func (x *MutationEnvelope) GetCreateCheckoutOrder() *CreateCheckoutOrder {
+ if x != nil {
+ if x, ok := x.Mutation.(*MutationEnvelope_CreateCheckoutOrder); ok {
+ return x.CreateCheckoutOrder
+ }
+ }
+ return nil
+}
+
+func (x *MutationEnvelope) GetSetCartItems() *SetCartRequest {
+ if x != nil {
+ if x, ok := x.Mutation.(*MutationEnvelope_SetCartItems); ok {
+ return x.SetCartItems
+ }
+ }
+ return nil
+}
+
+func (x *MutationEnvelope) GetOrderCompleted() *OrderCreated {
+ if x != nil {
+ if x, ok := x.Mutation.(*MutationEnvelope_OrderCompleted); ok {
+ return x.OrderCompleted
+ }
+ }
+ return nil
+}
+
+type isMutationEnvelope_Mutation interface {
+ isMutationEnvelope_Mutation()
+}
+
+type MutationEnvelope_AddRequest struct {
+ AddRequest *AddRequest `protobuf:"bytes,10,opt,name=add_request,json=addRequest,proto3,oneof"`
+}
+
+type MutationEnvelope_AddItem struct {
+ AddItem *AddItem `protobuf:"bytes,11,opt,name=add_item,json=addItem,proto3,oneof"`
+}
+
+type MutationEnvelope_RemoveItem struct {
+ RemoveItem *RemoveItem `protobuf:"bytes,12,opt,name=remove_item,json=removeItem,proto3,oneof"`
+}
+
+type MutationEnvelope_RemoveDelivery struct {
+ RemoveDelivery *RemoveDelivery `protobuf:"bytes,13,opt,name=remove_delivery,json=removeDelivery,proto3,oneof"`
+}
+
+type MutationEnvelope_ChangeQuantity struct {
+ ChangeQuantity *ChangeQuantity `protobuf:"bytes,14,opt,name=change_quantity,json=changeQuantity,proto3,oneof"`
+}
+
+type MutationEnvelope_SetDelivery struct {
+ SetDelivery *SetDelivery `protobuf:"bytes,15,opt,name=set_delivery,json=setDelivery,proto3,oneof"`
+}
+
+type MutationEnvelope_SetPickupPoint struct {
+ SetPickupPoint *SetPickupPoint `protobuf:"bytes,16,opt,name=set_pickup_point,json=setPickupPoint,proto3,oneof"`
+}
+
+type MutationEnvelope_CreateCheckoutOrder struct {
+ CreateCheckoutOrder *CreateCheckoutOrder `protobuf:"bytes,17,opt,name=create_checkout_order,json=createCheckoutOrder,proto3,oneof"`
+}
+
+type MutationEnvelope_SetCartItems struct {
+ SetCartItems *SetCartRequest `protobuf:"bytes,18,opt,name=set_cart_items,json=setCartItems,proto3,oneof"`
+}
+
+type MutationEnvelope_OrderCompleted struct {
+ OrderCompleted *OrderCreated `protobuf:"bytes,19,opt,name=order_completed,json=orderCompleted,proto3,oneof"`
+}
+
+func (*MutationEnvelope_AddRequest) isMutationEnvelope_Mutation() {}
+
+func (*MutationEnvelope_AddItem) isMutationEnvelope_Mutation() {}
+
+func (*MutationEnvelope_RemoveItem) isMutationEnvelope_Mutation() {}
+
+func (*MutationEnvelope_RemoveDelivery) isMutationEnvelope_Mutation() {}
+
+func (*MutationEnvelope_ChangeQuantity) isMutationEnvelope_Mutation() {}
+
+func (*MutationEnvelope_SetDelivery) isMutationEnvelope_Mutation() {}
+
+func (*MutationEnvelope_SetPickupPoint) isMutationEnvelope_Mutation() {}
+
+func (*MutationEnvelope_CreateCheckoutOrder) isMutationEnvelope_Mutation() {}
+
+func (*MutationEnvelope_SetCartItems) isMutationEnvelope_Mutation() {}
+
+func (*MutationEnvelope_OrderCompleted) isMutationEnvelope_Mutation() {}
+
+// MutationReply returns a legacy-style status code plus a JSON payload
+// holding either the updated cart state (on success) or an error string.
type MutationReply struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- StatusCode int32 `protobuf:"varint,1,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"`
- Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"` // JSON cart state or error string
+ state protoimpl.MessageState `protogen:"open.v1"`
+ StatusCode int32 `protobuf:"varint,1,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"`
+ // Exactly one of state or error will be set.
+ //
+ // Types that are valid to be assigned to Result:
+ //
+ // *MutationReply_State
+ // *MutationReply_Error
+ Result isMutationReply_Result `protobuf_oneof:"result"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -216,14 +304,48 @@ func (x *MutationReply) GetStatusCode() int32 {
return 0
}
-func (x *MutationReply) GetPayload() []byte {
+func (x *MutationReply) GetResult() isMutationReply_Result {
if x != nil {
- return x.Payload
+ return x.Result
}
return nil
}
-// StateRequest fetches current cart state without mutation.
+func (x *MutationReply) GetState() *CartState {
+ if x != nil {
+ if x, ok := x.Result.(*MutationReply_State); ok {
+ return x.State
+ }
+ }
+ return nil
+}
+
+func (x *MutationReply) GetError() string {
+ if x != nil {
+ if x, ok := x.Result.(*MutationReply_Error); ok {
+ return x.Error
+ }
+ }
+ return ""
+}
+
+type isMutationReply_Result interface {
+ isMutationReply_Result()
+}
+
+type MutationReply_State struct {
+ State *CartState `protobuf:"bytes,2,opt,name=state,proto3,oneof"`
+}
+
+type MutationReply_Error struct {
+ Error string `protobuf:"bytes,3,opt,name=error,proto3,oneof"`
+}
+
+func (*MutationReply_State) isMutationReply_Result() {}
+
+func (*MutationReply_Error) isMutationReply_Result() {}
+
+// StateRequest fetches current cart state without mutating.
type StateRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
CartId string `protobuf:"bytes,1,opt,name=cart_id,json=cartId,proto3" json:"cart_id,omitempty"`
@@ -268,18 +390,448 @@ func (x *StateRequest) GetCartId() string {
return ""
}
+// -----------------------------------------------------------------------------
+// CartState represents the full cart snapshot returned by state/mutation replies.
+// Replaces the previous raw JSON payload.
+// -----------------------------------------------------------------------------
+type CartState struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ CartId string `protobuf:"bytes,1,opt,name=cart_id,json=cartId,proto3" json:"cart_id,omitempty"`
+ Items []*CartItemState `protobuf:"bytes,2,rep,name=items,proto3" json:"items,omitempty"`
+ TotalPrice int64 `protobuf:"varint,3,opt,name=total_price,json=totalPrice,proto3" json:"total_price,omitempty"`
+ TotalTax int64 `protobuf:"varint,4,opt,name=total_tax,json=totalTax,proto3" json:"total_tax,omitempty"`
+ TotalDiscount int64 `protobuf:"varint,5,opt,name=total_discount,json=totalDiscount,proto3" json:"total_discount,omitempty"`
+ Deliveries []*DeliveryState `protobuf:"bytes,6,rep,name=deliveries,proto3" json:"deliveries,omitempty"`
+ PaymentInProgress bool `protobuf:"varint,7,opt,name=payment_in_progress,json=paymentInProgress,proto3" json:"payment_in_progress,omitempty"`
+ OrderReference string `protobuf:"bytes,8,opt,name=order_reference,json=orderReference,proto3" json:"order_reference,omitempty"`
+ PaymentStatus string `protobuf:"bytes,9,opt,name=payment_status,json=paymentStatus,proto3" json:"payment_status,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CartState) Reset() {
+ *x = CartState{}
+ mi := &file_cart_actor_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CartState) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CartState) ProtoMessage() {}
+
+func (x *CartState) ProtoReflect() protoreflect.Message {
+ mi := &file_cart_actor_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 CartState.ProtoReflect.Descriptor instead.
+func (*CartState) Descriptor() ([]byte, []int) {
+ return file_cart_actor_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *CartState) GetCartId() string {
+ if x != nil {
+ return x.CartId
+ }
+ return ""
+}
+
+func (x *CartState) GetItems() []*CartItemState {
+ if x != nil {
+ return x.Items
+ }
+ return nil
+}
+
+func (x *CartState) GetTotalPrice() int64 {
+ if x != nil {
+ return x.TotalPrice
+ }
+ return 0
+}
+
+func (x *CartState) GetTotalTax() int64 {
+ if x != nil {
+ return x.TotalTax
+ }
+ return 0
+}
+
+func (x *CartState) GetTotalDiscount() int64 {
+ if x != nil {
+ return x.TotalDiscount
+ }
+ return 0
+}
+
+func (x *CartState) GetDeliveries() []*DeliveryState {
+ if x != nil {
+ return x.Deliveries
+ }
+ return nil
+}
+
+func (x *CartState) GetPaymentInProgress() bool {
+ if x != nil {
+ return x.PaymentInProgress
+ }
+ return false
+}
+
+func (x *CartState) GetOrderReference() string {
+ if x != nil {
+ return x.OrderReference
+ }
+ return ""
+}
+
+func (x *CartState) GetPaymentStatus() string {
+ if x != nil {
+ return x.PaymentStatus
+ }
+ return ""
+}
+
+// Lightweight representation of an item in the cart
+type CartItemState struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+ SourceItemId int64 `protobuf:"varint,2,opt,name=source_item_id,json=sourceItemId,proto3" json:"source_item_id,omitempty"`
+ Sku string `protobuf:"bytes,3,opt,name=sku,proto3" json:"sku,omitempty"`
+ Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
+ UnitPrice int64 `protobuf:"varint,5,opt,name=unit_price,json=unitPrice,proto3" json:"unit_price,omitempty"`
+ Quantity int32 `protobuf:"varint,6,opt,name=quantity,proto3" json:"quantity,omitempty"`
+ TotalPrice int64 `protobuf:"varint,7,opt,name=total_price,json=totalPrice,proto3" json:"total_price,omitempty"`
+ TotalTax int64 `protobuf:"varint,8,opt,name=total_tax,json=totalTax,proto3" json:"total_tax,omitempty"`
+ OrgPrice int64 `protobuf:"varint,9,opt,name=org_price,json=orgPrice,proto3" json:"org_price,omitempty"`
+ TaxRate int32 `protobuf:"varint,10,opt,name=tax_rate,json=taxRate,proto3" json:"tax_rate,omitempty"`
+ TotalDiscount int64 `protobuf:"varint,11,opt,name=total_discount,json=totalDiscount,proto3" json:"total_discount,omitempty"`
+ Brand string `protobuf:"bytes,12,opt,name=brand,proto3" json:"brand,omitempty"`
+ Category string `protobuf:"bytes,13,opt,name=category,proto3" json:"category,omitempty"`
+ Category2 string `protobuf:"bytes,14,opt,name=category2,proto3" json:"category2,omitempty"`
+ Category3 string `protobuf:"bytes,15,opt,name=category3,proto3" json:"category3,omitempty"`
+ Category4 string `protobuf:"bytes,16,opt,name=category4,proto3" json:"category4,omitempty"`
+ Category5 string `protobuf:"bytes,17,opt,name=category5,proto3" json:"category5,omitempty"`
+ Image string `protobuf:"bytes,18,opt,name=image,proto3" json:"image,omitempty"`
+ ArticleType string `protobuf:"bytes,19,opt,name=article_type,json=articleType,proto3" json:"article_type,omitempty"`
+ SellerId string `protobuf:"bytes,20,opt,name=seller_id,json=sellerId,proto3" json:"seller_id,omitempty"`
+ SellerName string `protobuf:"bytes,21,opt,name=seller_name,json=sellerName,proto3" json:"seller_name,omitempty"`
+ Disclaimer string `protobuf:"bytes,22,opt,name=disclaimer,proto3" json:"disclaimer,omitempty"`
+ Outlet string `protobuf:"bytes,23,opt,name=outlet,proto3" json:"outlet,omitempty"`
+ StoreId string `protobuf:"bytes,24,opt,name=store_id,json=storeId,proto3" json:"store_id,omitempty"`
+ Stock int32 `protobuf:"varint,25,opt,name=stock,proto3" json:"stock,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CartItemState) Reset() {
+ *x = CartItemState{}
+ mi := &file_cart_actor_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CartItemState) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CartItemState) ProtoMessage() {}
+
+func (x *CartItemState) ProtoReflect() protoreflect.Message {
+ mi := &file_cart_actor_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 CartItemState.ProtoReflect.Descriptor instead.
+func (*CartItemState) Descriptor() ([]byte, []int) {
+ return file_cart_actor_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *CartItemState) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+func (x *CartItemState) GetSourceItemId() int64 {
+ if x != nil {
+ return x.SourceItemId
+ }
+ return 0
+}
+
+func (x *CartItemState) GetSku() string {
+ if x != nil {
+ return x.Sku
+ }
+ return ""
+}
+
+func (x *CartItemState) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *CartItemState) GetUnitPrice() int64 {
+ if x != nil {
+ return x.UnitPrice
+ }
+ return 0
+}
+
+func (x *CartItemState) GetQuantity() int32 {
+ if x != nil {
+ return x.Quantity
+ }
+ return 0
+}
+
+func (x *CartItemState) GetTotalPrice() int64 {
+ if x != nil {
+ return x.TotalPrice
+ }
+ return 0
+}
+
+func (x *CartItemState) GetTotalTax() int64 {
+ if x != nil {
+ return x.TotalTax
+ }
+ return 0
+}
+
+func (x *CartItemState) GetOrgPrice() int64 {
+ if x != nil {
+ return x.OrgPrice
+ }
+ return 0
+}
+
+func (x *CartItemState) GetTaxRate() int32 {
+ if x != nil {
+ return x.TaxRate
+ }
+ return 0
+}
+
+func (x *CartItemState) GetTotalDiscount() int64 {
+ if x != nil {
+ return x.TotalDiscount
+ }
+ return 0
+}
+
+func (x *CartItemState) GetBrand() string {
+ if x != nil {
+ return x.Brand
+ }
+ return ""
+}
+
+func (x *CartItemState) GetCategory() string {
+ if x != nil {
+ return x.Category
+ }
+ return ""
+}
+
+func (x *CartItemState) GetCategory2() string {
+ if x != nil {
+ return x.Category2
+ }
+ return ""
+}
+
+func (x *CartItemState) GetCategory3() string {
+ if x != nil {
+ return x.Category3
+ }
+ return ""
+}
+
+func (x *CartItemState) GetCategory4() string {
+ if x != nil {
+ return x.Category4
+ }
+ return ""
+}
+
+func (x *CartItemState) GetCategory5() string {
+ if x != nil {
+ return x.Category5
+ }
+ return ""
+}
+
+func (x *CartItemState) GetImage() string {
+ if x != nil {
+ return x.Image
+ }
+ return ""
+}
+
+func (x *CartItemState) GetArticleType() string {
+ if x != nil {
+ return x.ArticleType
+ }
+ return ""
+}
+
+func (x *CartItemState) GetSellerId() string {
+ if x != nil {
+ return x.SellerId
+ }
+ return ""
+}
+
+func (x *CartItemState) GetSellerName() string {
+ if x != nil {
+ return x.SellerName
+ }
+ return ""
+}
+
+func (x *CartItemState) GetDisclaimer() string {
+ if x != nil {
+ return x.Disclaimer
+ }
+ return ""
+}
+
+func (x *CartItemState) GetOutlet() string {
+ if x != nil {
+ return x.Outlet
+ }
+ return ""
+}
+
+func (x *CartItemState) GetStoreId() string {
+ if x != nil {
+ return x.StoreId
+ }
+ return ""
+}
+
+func (x *CartItemState) GetStock() int32 {
+ if x != nil {
+ return x.Stock
+ }
+ return 0
+}
+
+// Delivery / shipping entry
+type DeliveryState struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
+ Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"`
+ Price int64 `protobuf:"varint,3,opt,name=price,proto3" json:"price,omitempty"`
+ ItemIds []int64 `protobuf:"varint,4,rep,packed,name=item_ids,json=itemIds,proto3" json:"item_ids,omitempty"`
+ PickupPoint *PickupPoint `protobuf:"bytes,5,opt,name=pickup_point,json=pickupPoint,proto3" json:"pickup_point,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *DeliveryState) Reset() {
+ *x = DeliveryState{}
+ mi := &file_cart_actor_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *DeliveryState) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DeliveryState) ProtoMessage() {}
+
+func (x *DeliveryState) ProtoReflect() protoreflect.Message {
+ mi := &file_cart_actor_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 DeliveryState.ProtoReflect.Descriptor instead.
+func (*DeliveryState) Descriptor() ([]byte, []int) {
+ return file_cart_actor_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *DeliveryState) GetId() int64 {
+ if x != nil {
+ return x.Id
+ }
+ return 0
+}
+
+func (x *DeliveryState) GetProvider() string {
+ if x != nil {
+ return x.Provider
+ }
+ return ""
+}
+
+func (x *DeliveryState) GetPrice() int64 {
+ if x != nil {
+ return x.Price
+ }
+ return 0
+}
+
+func (x *DeliveryState) GetItemIds() []int64 {
+ if x != nil {
+ return x.ItemIds
+ }
+ return nil
+}
+
+func (x *DeliveryState) GetPickupPoint() *PickupPoint {
+ if x != nil {
+ return x.PickupPoint
+ }
+ return nil
+}
+
// StateReply mirrors MutationReply for consistency.
type StateReply struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- StatusCode int32 `protobuf:"varint,1,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"`
- Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"` // JSON cart state or error string
+ state protoimpl.MessageState `protogen:"open.v1"`
+ StatusCode int32 `protobuf:"varint,1,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"`
+ // Types that are valid to be assigned to Result:
+ //
+ // *StateReply_State
+ // *StateReply_Error
+ Result isStateReply_Result `protobuf_oneof:"result"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *StateReply) Reset() {
*x = StateReply{}
- mi := &file_cart_actor_proto_msgTypes[3]
+ mi := &file_cart_actor_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -291,7 +843,7 @@ func (x *StateReply) String() string {
func (*StateReply) ProtoMessage() {}
func (x *StateReply) ProtoReflect() protoreflect.Message {
- mi := &file_cart_actor_proto_msgTypes[3]
+ mi := &file_cart_actor_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -304,7 +856,7 @@ func (x *StateReply) ProtoReflect() protoreflect.Message {
// Deprecated: Use StateReply.ProtoReflect.Descriptor instead.
func (*StateReply) Descriptor() ([]byte, []int) {
- return file_cart_actor_proto_rawDescGZIP(), []int{3}
+ return file_cart_actor_proto_rawDescGZIP(), []int{6}
}
func (x *StateReply) GetStatusCode() int32 {
@@ -314,51 +866,139 @@ func (x *StateReply) GetStatusCode() int32 {
return 0
}
-func (x *StateReply) GetPayload() []byte {
+func (x *StateReply) GetResult() isStateReply_Result {
if x != nil {
- return x.Payload
+ return x.Result
}
return nil
}
+func (x *StateReply) GetState() *CartState {
+ if x != nil {
+ if x, ok := x.Result.(*StateReply_State); ok {
+ return x.State
+ }
+ }
+ return nil
+}
+
+func (x *StateReply) GetError() string {
+ if x != nil {
+ if x, ok := x.Result.(*StateReply_Error); ok {
+ return x.Error
+ }
+ }
+ return ""
+}
+
+type isStateReply_Result interface {
+ isStateReply_Result()
+}
+
+type StateReply_State struct {
+ State *CartState `protobuf:"bytes,2,opt,name=state,proto3,oneof"`
+}
+
+type StateReply_Error struct {
+ Error string `protobuf:"bytes,3,opt,name=error,proto3,oneof"`
+}
+
+func (*StateReply_State) isStateReply_Result() {}
+
+func (*StateReply_Error) isStateReply_Result() {}
+
var File_cart_actor_proto protoreflect.FileDescriptor
const file_cart_actor_proto_rawDesc = "" +
"\n" +
- "\x10cart_actor.proto\x12\bmessages\"\x9b\x01\n" +
- "\x0fMutationRequest\x12\x17\n" +
- "\acart_id\x18\x01 \x01(\tR\x06cartId\x12*\n" +
- "\x04type\x18\x02 \x01(\x0e2\x16.messages.MutationTypeR\x04type\x12\x18\n" +
- "\apayload\x18\x03 \x01(\fR\apayload\x12)\n" +
- "\x10client_timestamp\x18\x04 \x01(\x03R\x0fclientTimestamp\"J\n" +
+ "\x10cart_actor.proto\x12\bmessages\x1a\x0emessages.proto\"\xea\x05\n" +
+ "\x10MutationEnvelope\x12\x17\n" +
+ "\acart_id\x18\x01 \x01(\tR\x06cartId\x12)\n" +
+ "\x10client_timestamp\x18\x02 \x01(\x03R\x0fclientTimestamp\x127\n" +
+ "\vadd_request\x18\n" +
+ " \x01(\v2\x14.messages.AddRequestH\x00R\n" +
+ "addRequest\x12.\n" +
+ "\badd_item\x18\v \x01(\v2\x11.messages.AddItemH\x00R\aaddItem\x127\n" +
+ "\vremove_item\x18\f \x01(\v2\x14.messages.RemoveItemH\x00R\n" +
+ "removeItem\x12C\n" +
+ "\x0fremove_delivery\x18\r \x01(\v2\x18.messages.RemoveDeliveryH\x00R\x0eremoveDelivery\x12C\n" +
+ "\x0fchange_quantity\x18\x0e \x01(\v2\x18.messages.ChangeQuantityH\x00R\x0echangeQuantity\x12:\n" +
+ "\fset_delivery\x18\x0f \x01(\v2\x15.messages.SetDeliveryH\x00R\vsetDelivery\x12D\n" +
+ "\x10set_pickup_point\x18\x10 \x01(\v2\x18.messages.SetPickupPointH\x00R\x0esetPickupPoint\x12S\n" +
+ "\x15create_checkout_order\x18\x11 \x01(\v2\x1d.messages.CreateCheckoutOrderH\x00R\x13createCheckoutOrder\x12@\n" +
+ "\x0eset_cart_items\x18\x12 \x01(\v2\x18.messages.SetCartRequestH\x00R\fsetCartItems\x12A\n" +
+ "\x0forder_completed\x18\x13 \x01(\v2\x16.messages.OrderCreatedH\x00R\x0eorderCompletedB\n" +
+ "\n" +
+ "\bmutation\"\x7f\n" +
"\rMutationReply\x12\x1f\n" +
"\vstatus_code\x18\x01 \x01(\x05R\n" +
- "statusCode\x12\x18\n" +
- "\apayload\x18\x02 \x01(\fR\apayload\"'\n" +
+ "statusCode\x12+\n" +
+ "\x05state\x18\x02 \x01(\v2\x13.messages.CartStateH\x00R\x05state\x12\x16\n" +
+ "\x05error\x18\x03 \x01(\tH\x00R\x05errorB\b\n" +
+ "\x06result\"'\n" +
"\fStateRequest\x12\x17\n" +
- "\acart_id\x18\x01 \x01(\tR\x06cartId\"G\n" +
+ "\acart_id\x18\x01 \x01(\tR\x06cartId\"\xf1\x02\n" +
+ "\tCartState\x12\x17\n" +
+ "\acart_id\x18\x01 \x01(\tR\x06cartId\x12-\n" +
+ "\x05items\x18\x02 \x03(\v2\x17.messages.CartItemStateR\x05items\x12\x1f\n" +
+ "\vtotal_price\x18\x03 \x01(\x03R\n" +
+ "totalPrice\x12\x1b\n" +
+ "\ttotal_tax\x18\x04 \x01(\x03R\btotalTax\x12%\n" +
+ "\x0etotal_discount\x18\x05 \x01(\x03R\rtotalDiscount\x127\n" +
+ "\n" +
+ "deliveries\x18\x06 \x03(\v2\x17.messages.DeliveryStateR\n" +
+ "deliveries\x12.\n" +
+ "\x13payment_in_progress\x18\a \x01(\bR\x11paymentInProgress\x12'\n" +
+ "\x0forder_reference\x18\b \x01(\tR\x0eorderReference\x12%\n" +
+ "\x0epayment_status\x18\t \x01(\tR\rpaymentStatus\"\xcd\x05\n" +
+ "\rCartItemState\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\x03R\x02id\x12$\n" +
+ "\x0esource_item_id\x18\x02 \x01(\x03R\fsourceItemId\x12\x10\n" +
+ "\x03sku\x18\x03 \x01(\tR\x03sku\x12\x12\n" +
+ "\x04name\x18\x04 \x01(\tR\x04name\x12\x1d\n" +
+ "\n" +
+ "unit_price\x18\x05 \x01(\x03R\tunitPrice\x12\x1a\n" +
+ "\bquantity\x18\x06 \x01(\x05R\bquantity\x12\x1f\n" +
+ "\vtotal_price\x18\a \x01(\x03R\n" +
+ "totalPrice\x12\x1b\n" +
+ "\ttotal_tax\x18\b \x01(\x03R\btotalTax\x12\x1b\n" +
+ "\torg_price\x18\t \x01(\x03R\borgPrice\x12\x19\n" +
+ "\btax_rate\x18\n" +
+ " \x01(\x05R\ataxRate\x12%\n" +
+ "\x0etotal_discount\x18\v \x01(\x03R\rtotalDiscount\x12\x14\n" +
+ "\x05brand\x18\f \x01(\tR\x05brand\x12\x1a\n" +
+ "\bcategory\x18\r \x01(\tR\bcategory\x12\x1c\n" +
+ "\tcategory2\x18\x0e \x01(\tR\tcategory2\x12\x1c\n" +
+ "\tcategory3\x18\x0f \x01(\tR\tcategory3\x12\x1c\n" +
+ "\tcategory4\x18\x10 \x01(\tR\tcategory4\x12\x1c\n" +
+ "\tcategory5\x18\x11 \x01(\tR\tcategory5\x12\x14\n" +
+ "\x05image\x18\x12 \x01(\tR\x05image\x12!\n" +
+ "\farticle_type\x18\x13 \x01(\tR\varticleType\x12\x1b\n" +
+ "\tseller_id\x18\x14 \x01(\tR\bsellerId\x12\x1f\n" +
+ "\vseller_name\x18\x15 \x01(\tR\n" +
+ "sellerName\x12\x1e\n" +
+ "\n" +
+ "disclaimer\x18\x16 \x01(\tR\n" +
+ "disclaimer\x12\x16\n" +
+ "\x06outlet\x18\x17 \x01(\tR\x06outlet\x12\x19\n" +
+ "\bstore_id\x18\x18 \x01(\tR\astoreId\x12\x14\n" +
+ "\x05stock\x18\x19 \x01(\x05R\x05stock\"\xa6\x01\n" +
+ "\rDeliveryState\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\x03R\x02id\x12\x1a\n" +
+ "\bprovider\x18\x02 \x01(\tR\bprovider\x12\x14\n" +
+ "\x05price\x18\x03 \x01(\x03R\x05price\x12\x19\n" +
+ "\bitem_ids\x18\x04 \x03(\x03R\aitemIds\x128\n" +
+ "\fpickup_point\x18\x05 \x01(\v2\x15.messages.PickupPointR\vpickupPoint\"|\n" +
"\n" +
"StateReply\x12\x1f\n" +
"\vstatus_code\x18\x01 \x01(\x05R\n" +
- "statusCode\x12\x18\n" +
- "\apayload\x18\x02 \x01(\fR\apayload*\xcd\x02\n" +
- "\fMutationType\x12\x1d\n" +
- "\x19MUTATION_TYPE_UNSPECIFIED\x10\x00\x12\x18\n" +
- "\x14MUTATION_ADD_REQUEST\x10\x01\x12\x15\n" +
- "\x11MUTATION_ADD_ITEM\x10\x02\x12\x18\n" +
- "\x14MUTATION_REMOVE_ITEM\x10\x04\x12\x1c\n" +
- "\x18MUTATION_REMOVE_DELIVERY\x10\x05\x12\x1c\n" +
- "\x18MUTATION_CHANGE_QUANTITY\x10\x06\x12\x19\n" +
- "\x15MUTATION_SET_DELIVERY\x10\a\x12\x1d\n" +
- "\x19MUTATION_SET_PICKUP_POINT\x10\b\x12\"\n" +
- "\x1eMUTATION_CREATE_CHECKOUT_ORDER\x10\t\x12\x1b\n" +
- "\x17MUTATION_SET_CART_ITEMS\x10\n" +
- "\x12\x1c\n" +
- "\x18MUTATION_ORDER_COMPLETED\x10\v2\x83\x01\n" +
- "\tCartActor\x12<\n" +
- "\x06Mutate\x12\x19.messages.MutationRequest\x1a\x17.messages.MutationReply\x128\n" +
- "\bGetState\x12\x16.messages.StateRequest\x1a\x14.messages.StateReplyB\fZ\n" +
- ".;messagesb\x06proto3"
+ "statusCode\x12+\n" +
+ "\x05state\x18\x02 \x01(\v2\x13.messages.CartStateH\x00R\x05state\x12\x16\n" +
+ "\x05error\x18\x03 \x01(\tH\x00R\x05errorB\b\n" +
+ "\x06result2\x84\x01\n" +
+ "\tCartActor\x12=\n" +
+ "\x06Mutate\x12\x1a.messages.MutationEnvelope\x1a\x17.messages.MutationReply\x128\n" +
+ "\bGetState\x12\x16.messages.StateRequest\x1a\x14.messages.StateReplyB.Z,git.tornberg.me/go-cart-actor/proto;messagesb\x06proto3"
var (
file_cart_actor_proto_rawDescOnce sync.Once
@@ -372,26 +1012,52 @@ func file_cart_actor_proto_rawDescGZIP() []byte {
return file_cart_actor_proto_rawDescData
}
-var file_cart_actor_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
-var file_cart_actor_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_cart_actor_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_cart_actor_proto_goTypes = []any{
- (MutationType)(0), // 0: messages.MutationType
- (*MutationRequest)(nil), // 1: messages.MutationRequest
- (*MutationReply)(nil), // 2: messages.MutationReply
- (*StateRequest)(nil), // 3: messages.StateRequest
- (*StateReply)(nil), // 4: messages.StateReply
+ (*MutationEnvelope)(nil), // 0: messages.MutationEnvelope
+ (*MutationReply)(nil), // 1: messages.MutationReply
+ (*StateRequest)(nil), // 2: messages.StateRequest
+ (*CartState)(nil), // 3: messages.CartState
+ (*CartItemState)(nil), // 4: messages.CartItemState
+ (*DeliveryState)(nil), // 5: messages.DeliveryState
+ (*StateReply)(nil), // 6: messages.StateReply
+ (*AddRequest)(nil), // 7: messages.AddRequest
+ (*AddItem)(nil), // 8: messages.AddItem
+ (*RemoveItem)(nil), // 9: messages.RemoveItem
+ (*RemoveDelivery)(nil), // 10: messages.RemoveDelivery
+ (*ChangeQuantity)(nil), // 11: messages.ChangeQuantity
+ (*SetDelivery)(nil), // 12: messages.SetDelivery
+ (*SetPickupPoint)(nil), // 13: messages.SetPickupPoint
+ (*CreateCheckoutOrder)(nil), // 14: messages.CreateCheckoutOrder
+ (*SetCartRequest)(nil), // 15: messages.SetCartRequest
+ (*OrderCreated)(nil), // 16: messages.OrderCreated
+ (*PickupPoint)(nil), // 17: messages.PickupPoint
}
var file_cart_actor_proto_depIdxs = []int32{
- 0, // 0: messages.MutationRequest.type:type_name -> messages.MutationType
- 1, // 1: messages.CartActor.Mutate:input_type -> messages.MutationRequest
- 3, // 2: messages.CartActor.GetState:input_type -> messages.StateRequest
- 2, // 3: messages.CartActor.Mutate:output_type -> messages.MutationReply
- 4, // 4: messages.CartActor.GetState:output_type -> messages.StateReply
- 3, // [3:5] is the sub-list for method output_type
- 1, // [1:3] is the sub-list for method input_type
- 1, // [1:1] is the sub-list for extension type_name
- 1, // [1:1] is the sub-list for extension extendee
- 0, // [0:1] is the sub-list for field type_name
+ 7, // 0: messages.MutationEnvelope.add_request:type_name -> messages.AddRequest
+ 8, // 1: messages.MutationEnvelope.add_item:type_name -> messages.AddItem
+ 9, // 2: messages.MutationEnvelope.remove_item:type_name -> messages.RemoveItem
+ 10, // 3: messages.MutationEnvelope.remove_delivery:type_name -> messages.RemoveDelivery
+ 11, // 4: messages.MutationEnvelope.change_quantity:type_name -> messages.ChangeQuantity
+ 12, // 5: messages.MutationEnvelope.set_delivery:type_name -> messages.SetDelivery
+ 13, // 6: messages.MutationEnvelope.set_pickup_point:type_name -> messages.SetPickupPoint
+ 14, // 7: messages.MutationEnvelope.create_checkout_order:type_name -> messages.CreateCheckoutOrder
+ 15, // 8: messages.MutationEnvelope.set_cart_items:type_name -> messages.SetCartRequest
+ 16, // 9: messages.MutationEnvelope.order_completed:type_name -> messages.OrderCreated
+ 3, // 10: messages.MutationReply.state:type_name -> messages.CartState
+ 4, // 11: messages.CartState.items:type_name -> messages.CartItemState
+ 5, // 12: messages.CartState.deliveries:type_name -> messages.DeliveryState
+ 17, // 13: messages.DeliveryState.pickup_point:type_name -> messages.PickupPoint
+ 3, // 14: messages.StateReply.state:type_name -> messages.CartState
+ 0, // 15: messages.CartActor.Mutate:input_type -> messages.MutationEnvelope
+ 2, // 16: messages.CartActor.GetState:input_type -> messages.StateRequest
+ 1, // 17: messages.CartActor.Mutate:output_type -> messages.MutationReply
+ 6, // 18: messages.CartActor.GetState:output_type -> messages.StateReply
+ 17, // [17:19] is the sub-list for method output_type
+ 15, // [15:17] is the sub-list for method input_type
+ 15, // [15:15] is the sub-list for extension type_name
+ 15, // [15:15] is the sub-list for extension extendee
+ 0, // [0:15] is the sub-list for field type_name
}
func init() { file_cart_actor_proto_init() }
@@ -399,19 +1065,39 @@ func file_cart_actor_proto_init() {
if File_cart_actor_proto != nil {
return
}
+ file_messages_proto_init()
+ file_cart_actor_proto_msgTypes[0].OneofWrappers = []any{
+ (*MutationEnvelope_AddRequest)(nil),
+ (*MutationEnvelope_AddItem)(nil),
+ (*MutationEnvelope_RemoveItem)(nil),
+ (*MutationEnvelope_RemoveDelivery)(nil),
+ (*MutationEnvelope_ChangeQuantity)(nil),
+ (*MutationEnvelope_SetDelivery)(nil),
+ (*MutationEnvelope_SetPickupPoint)(nil),
+ (*MutationEnvelope_CreateCheckoutOrder)(nil),
+ (*MutationEnvelope_SetCartItems)(nil),
+ (*MutationEnvelope_OrderCompleted)(nil),
+ }
+ file_cart_actor_proto_msgTypes[1].OneofWrappers = []any{
+ (*MutationReply_State)(nil),
+ (*MutationReply_Error)(nil),
+ }
+ file_cart_actor_proto_msgTypes[6].OneofWrappers = []any{
+ (*StateReply_State)(nil),
+ (*StateReply_Error)(nil),
+ }
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_cart_actor_proto_rawDesc), len(file_cart_actor_proto_rawDesc)),
- NumEnums: 1,
- NumMessages: 4,
+ NumEnums: 0,
+ NumMessages: 7,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_cart_actor_proto_goTypes,
DependencyIndexes: file_cart_actor_proto_depIdxs,
- EnumInfos: file_cart_actor_proto_enumTypes,
MessageInfos: file_cart_actor_proto_msgTypes,
}.Build()
File_cart_actor_proto = out.File
diff --git a/proto/cart_actor.proto b/proto/cart_actor.proto
index 7aa5aaf..b09a1c2 100644
--- a/proto/cart_actor.proto
+++ b/proto/cart_actor.proto
@@ -2,88 +2,141 @@ syntax = "proto3";
package messages;
-option go_package = ".;messages";
+option go_package = "git.tornberg.me/go-cart-actor/proto;messages";
+import "messages.proto";
// -----------------------------------------------------------------------------
-// Cart Actor gRPC API (Envelope Variant)
+// Cart Actor gRPC API (oneof Envelope Variant)
// -----------------------------------------------------------------------------
-// This service replaces the legacy custom TCP frame protocol used on port 1337.
-// It keeps the existing per-mutation proto messages (defined in messages.proto)
-// serialized into an opaque `bytes payload` field for minimal refactor cost.
-// The numeric values in MutationType MUST match the legacy message type
-// constants (see message-types.go) so persisted event logs replay correctly.
+// This version removes the legacy numeric MutationType enum + raw bytes payload
+// approach and replaces it with a strongly typed oneof envelope. Each concrete
+// mutation proto is embedded directly, enabling:
+// * Type-safe routing server-side (simple type switch on the oneof).
+// * Direct persistence of MutationEnvelope messages (no custom binary header).
+// * Elimination of the legacy message handler registry.
+//
+// NOTE: Regenerate Go code after editing:
+// protoc --go_out=. --go_opt=paths=source_relative \
+// --go-grpc_out=. --go-grpc_opt=paths=source_relative \
+// proto/cart_actor.proto proto/messages.proto
// -----------------------------------------------------------------------------
-// MutationType corresponds 1:1 with the legacy uint16 message type constants.
-enum MutationType {
- MUTATION_TYPE_UNSPECIFIED = 0;
- MUTATION_ADD_REQUEST = 1;
- MUTATION_ADD_ITEM = 2;
- // (3 was unused / reserved in legacy framing)
- MUTATION_REMOVE_ITEM = 4;
- MUTATION_REMOVE_DELIVERY = 5;
- MUTATION_CHANGE_QUANTITY = 6;
- MUTATION_SET_DELIVERY = 7;
- MUTATION_SET_PICKUP_POINT = 8;
- MUTATION_CREATE_CHECKOUT_ORDER = 9;
- MUTATION_SET_CART_ITEMS = 10;
- MUTATION_ORDER_COMPLETED = 11;
+// MutationEnvelope carries exactly one mutation plus metadata.
+// client_timestamp:
+// - Optional Unix timestamp provided by the client.
+// - If zero the server MAY overwrite with its local time.
+message MutationEnvelope {
+ string cart_id = 1;
+ int64 client_timestamp = 2;
+
+ oneof mutation {
+ AddRequest add_request = 10;
+ AddItem add_item = 11;
+ RemoveItem remove_item = 12;
+ RemoveDelivery remove_delivery = 13;
+ ChangeQuantity change_quantity = 14;
+ SetDelivery set_delivery = 15;
+ SetPickupPoint set_pickup_point = 16;
+ CreateCheckoutOrder create_checkout_order = 17;
+ SetCartRequest set_cart_items = 18;
+ OrderCreated order_completed = 19;
+ }
}
-// MutationRequest is an envelope:
-// - cart_id: string form of CartId (legacy 16-byte array truncated/padded).
-// - type: mutation kind (see enum).
-// - payload: serialized underlying proto message (AddRequest, AddItem, etc.).
-// - client_timestamp: optional unix timestamp; server sets if zero.
-message MutationRequest {
- string cart_id = 1;
- MutationType type = 2;
- bytes payload = 3;
- int64 client_timestamp = 4;
-}
-
-// MutationReply returns a status code (legacy semantics) plus a JSON payload
-// representing the full cart state (or an error message if non-200).
+// MutationReply returns a legacy-style status code plus a JSON payload
+// holding either the updated cart state (on success) or an error string.
message MutationReply {
int32 status_code = 1;
- bytes payload = 2; // JSON cart state or error string
+
+ // Exactly one of state or error will be set.
+ oneof result {
+ CartState state = 2;
+ string error = 3;
+ }
}
-// StateRequest fetches current cart state without mutation.
+// StateRequest fetches current cart state without mutating.
message StateRequest {
string cart_id = 1;
}
+// -----------------------------------------------------------------------------
+// CartState represents the full cart snapshot returned by state/mutation replies.
+// Replaces the previous raw JSON payload.
+// -----------------------------------------------------------------------------
+message CartState {
+ string cart_id = 1;
+ repeated CartItemState items = 2;
+ int64 total_price = 3;
+ int64 total_tax = 4;
+ int64 total_discount = 5;
+ repeated DeliveryState deliveries = 6;
+ bool payment_in_progress = 7;
+ string order_reference = 8;
+ string payment_status = 9;
+}
+
+// Lightweight representation of an item in the cart
+message CartItemState {
+ int64 id = 1;
+ int64 source_item_id = 2;
+ string sku = 3;
+ string name = 4;
+ int64 unit_price = 5;
+ int32 quantity = 6;
+ int64 total_price = 7;
+ int64 total_tax = 8;
+ int64 org_price = 9;
+ int32 tax_rate = 10;
+ int64 total_discount = 11;
+ string brand = 12;
+ string category = 13;
+ string category2 = 14;
+ string category3 = 15;
+ string category4 = 16;
+ string category5 = 17;
+ string image = 18;
+ string article_type = 19;
+ string seller_id = 20;
+ string seller_name = 21;
+ string disclaimer = 22;
+ string outlet = 23;
+ string store_id = 24;
+ int32 stock = 25;
+}
+
+// Delivery / shipping entry
+message DeliveryState {
+ int64 id = 1;
+ string provider = 2;
+ int64 price = 3;
+ repeated int64 item_ids = 4;
+ PickupPoint pickup_point = 5;
+}
+
// StateReply mirrors MutationReply for consistency.
message StateReply {
int32 status_code = 1;
- bytes payload = 2; // JSON cart state or error string
+
+ oneof result {
+ CartState state = 2;
+ string error = 3;
+ }
}
// CartActor exposes mutation and state retrieval for remote grains.
service CartActor {
// Mutate applies a single mutation to a cart, creating the cart lazily if needed.
- rpc Mutate(MutationRequest) returns (MutationReply);
+ rpc Mutate(MutationEnvelope) returns (MutationReply);
// GetState retrieves the cart's current state (JSON).
rpc GetState(StateRequest) returns (StateReply);
}
// -----------------------------------------------------------------------------
-// Notes:
-//
-// 1. Generation:
-// protoc --go_out=. --go_opt=paths=source_relative \
-// --go-grpc_out=. --go-grpc_opt=paths=source_relative \
-// cart_actor.proto
-//
-// 2. Underlying mutation payloads originate from messages.proto definitions.
-// The server side will route based on MutationType and decode payload bytes
-// using existing handler registry logic.
-//
-// 3. Future Enhancements:
-// - Replace JSON state payload with a strongly typed CartState proto.
-// - Add streaming RPC (e.g. WatchState) for live updates.
-// - Migrate control plane (negotiate/ownership) into a separate proto
-// (control_plane.proto) as per the migration plan.
+// Future Enhancements:
+// * Replace JSON state payload with a strongly typed CartState proto.
+// * Add streaming RPC (e.g., WatchState) for live updates.
+// * Add batch mutations (repeated MutationEnvelope) if performance requires.
+// * Introduce optimistic concurrency via version fields if external writers appear.
// -----------------------------------------------------------------------------
diff --git a/proto/cart_actor_grpc.pb.go b/proto/cart_actor_grpc.pb.go
index 6d9444a..247f758 100644
--- a/proto/cart_actor_grpc.pb.go
+++ b/proto/cart_actor_grpc.pb.go
@@ -30,7 +30,7 @@ const (
// CartActor exposes mutation and state retrieval for remote grains.
type CartActorClient interface {
// Mutate applies a single mutation to a cart, creating the cart lazily if needed.
- Mutate(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*MutationReply, error)
+ Mutate(ctx context.Context, in *MutationEnvelope, opts ...grpc.CallOption) (*MutationReply, error)
// GetState retrieves the cart's current state (JSON).
GetState(ctx context.Context, in *StateRequest, opts ...grpc.CallOption) (*StateReply, error)
}
@@ -43,7 +43,7 @@ func NewCartActorClient(cc grpc.ClientConnInterface) CartActorClient {
return &cartActorClient{cc}
}
-func (c *cartActorClient) Mutate(ctx context.Context, in *MutationRequest, opts ...grpc.CallOption) (*MutationReply, error) {
+func (c *cartActorClient) Mutate(ctx context.Context, in *MutationEnvelope, opts ...grpc.CallOption) (*MutationReply, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(MutationReply)
err := c.cc.Invoke(ctx, CartActor_Mutate_FullMethodName, in, out, cOpts...)
@@ -70,7 +70,7 @@ func (c *cartActorClient) GetState(ctx context.Context, in *StateRequest, opts .
// CartActor exposes mutation and state retrieval for remote grains.
type CartActorServer interface {
// Mutate applies a single mutation to a cart, creating the cart lazily if needed.
- Mutate(context.Context, *MutationRequest) (*MutationReply, error)
+ Mutate(context.Context, *MutationEnvelope) (*MutationReply, error)
// GetState retrieves the cart's current state (JSON).
GetState(context.Context, *StateRequest) (*StateReply, error)
mustEmbedUnimplementedCartActorServer()
@@ -83,7 +83,7 @@ type CartActorServer interface {
// pointer dereference when methods are called.
type UnimplementedCartActorServer struct{}
-func (UnimplementedCartActorServer) Mutate(context.Context, *MutationRequest) (*MutationReply, error) {
+func (UnimplementedCartActorServer) Mutate(context.Context, *MutationEnvelope) (*MutationReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method Mutate not implemented")
}
func (UnimplementedCartActorServer) GetState(context.Context, *StateRequest) (*StateReply, error) {
@@ -111,7 +111,7 @@ func RegisterCartActorServer(s grpc.ServiceRegistrar, srv CartActorServer) {
}
func _CartActor_Mutate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
- in := new(MutationRequest)
+ in := new(MutationEnvelope)
if err := dec(in); err != nil {
return nil, err
}
@@ -123,7 +123,7 @@ func _CartActor_Mutate_Handler(srv interface{}, ctx context.Context, dec func(in
FullMethod: CartActor_Mutate_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
- return srv.(CartActorServer).Mutate(ctx, req.(*MutationRequest))
+ return srv.(CartActorServer).Mutate(ctx, req.(*MutationEnvelope))
}
return interceptor(ctx, in, info, handler)
}
diff --git a/proto/control_plane.pb.go b/proto/control_plane.pb.go
index 0b0252a..b166977 100644
--- a/proto/control_plane.pb.go
+++ b/proto/control_plane.pb.go
@@ -427,8 +427,7 @@ const file_control_plane_proto_rawDesc = "" +
"\n" +
"GetCartIds\x12\x0f.messages.Empty\x1a\x16.messages.CartIdsReply\x12F\n" +
"\fConfirmOwner\x12\x1c.messages.OwnerChangeRequest\x1a\x18.messages.OwnerChangeAck\x12<\n" +
- "\aClosing\x12\x17.messages.ClosingNotice\x1a\x18.messages.OwnerChangeAckB\fZ\n" +
- ".;messagesb\x06proto3"
+ "\aClosing\x12\x17.messages.ClosingNotice\x1a\x18.messages.OwnerChangeAckB.Z,git.tornberg.me/go-cart-actor/proto;messagesb\x06proto3"
var (
file_control_plane_proto_rawDescOnce sync.Once
diff --git a/proto/control_plane.proto b/proto/control_plane.proto
index d528020..77234cd 100644
--- a/proto/control_plane.proto
+++ b/proto/control_plane.proto
@@ -2,7 +2,7 @@ syntax = "proto3";
package messages;
-option go_package = ".;messages";
+option go_package = "git.tornberg.me/go-cart-actor/proto;messages";
// -----------------------------------------------------------------------------
// Control Plane gRPC API
diff --git a/proto/messages.pb.go b/proto/messages.pb.go
index 755a0bf..e69de29 100644
--- a/proto/messages.pb.go
+++ b/proto/messages.pb.go
@@ -1,1066 +0,0 @@
-// Code generated by protoc-gen-go. DO NOT EDIT.
-// versions:
-// protoc-gen-go v1.36.10
-// protoc v3.21.12
-// source: messages.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)
-)
-
-type AddRequest struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- Quantity int32 `protobuf:"varint,1,opt,name=quantity,proto3" json:"quantity,omitempty"`
- Sku string `protobuf:"bytes,2,opt,name=sku,proto3" json:"sku,omitempty"`
- Country string `protobuf:"bytes,3,opt,name=country,proto3" json:"country,omitempty"`
- StoreId *string `protobuf:"bytes,4,opt,name=storeId,proto3,oneof" json:"storeId,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *AddRequest) Reset() {
- *x = AddRequest{}
- mi := &file_messages_proto_msgTypes[0]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *AddRequest) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*AddRequest) ProtoMessage() {}
-
-func (x *AddRequest) ProtoReflect() protoreflect.Message {
- mi := &file_messages_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 AddRequest.ProtoReflect.Descriptor instead.
-func (*AddRequest) Descriptor() ([]byte, []int) {
- return file_messages_proto_rawDescGZIP(), []int{0}
-}
-
-func (x *AddRequest) GetQuantity() int32 {
- if x != nil {
- return x.Quantity
- }
- return 0
-}
-
-func (x *AddRequest) GetSku() string {
- if x != nil {
- return x.Sku
- }
- return ""
-}
-
-func (x *AddRequest) GetCountry() string {
- if x != nil {
- return x.Country
- }
- return ""
-}
-
-func (x *AddRequest) GetStoreId() string {
- if x != nil && x.StoreId != nil {
- return *x.StoreId
- }
- return ""
-}
-
-type SetCartRequest struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- Items []*AddRequest `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *SetCartRequest) Reset() {
- *x = SetCartRequest{}
- mi := &file_messages_proto_msgTypes[1]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *SetCartRequest) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*SetCartRequest) ProtoMessage() {}
-
-func (x *SetCartRequest) ProtoReflect() protoreflect.Message {
- mi := &file_messages_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 SetCartRequest.ProtoReflect.Descriptor instead.
-func (*SetCartRequest) Descriptor() ([]byte, []int) {
- return file_messages_proto_rawDescGZIP(), []int{1}
-}
-
-func (x *SetCartRequest) GetItems() []*AddRequest {
- if x != nil {
- return x.Items
- }
- return nil
-}
-
-type AddItem struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- ItemId int64 `protobuf:"varint,1,opt,name=item_id,json=itemId,proto3" json:"item_id,omitempty"`
- Quantity int32 `protobuf:"varint,2,opt,name=quantity,proto3" json:"quantity,omitempty"`
- Price int64 `protobuf:"varint,3,opt,name=price,proto3" json:"price,omitempty"`
- OrgPrice int64 `protobuf:"varint,9,opt,name=orgPrice,proto3" json:"orgPrice,omitempty"`
- Sku string `protobuf:"bytes,4,opt,name=sku,proto3" json:"sku,omitempty"`
- Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"`
- Image string `protobuf:"bytes,6,opt,name=image,proto3" json:"image,omitempty"`
- Stock int32 `protobuf:"varint,7,opt,name=stock,proto3" json:"stock,omitempty"`
- Tax int32 `protobuf:"varint,8,opt,name=tax,proto3" json:"tax,omitempty"`
- Brand string `protobuf:"bytes,13,opt,name=brand,proto3" json:"brand,omitempty"`
- Category string `protobuf:"bytes,14,opt,name=category,proto3" json:"category,omitempty"`
- Category2 string `protobuf:"bytes,15,opt,name=category2,proto3" json:"category2,omitempty"`
- Category3 string `protobuf:"bytes,16,opt,name=category3,proto3" json:"category3,omitempty"`
- Category4 string `protobuf:"bytes,17,opt,name=category4,proto3" json:"category4,omitempty"`
- Category5 string `protobuf:"bytes,18,opt,name=category5,proto3" json:"category5,omitempty"`
- Disclaimer string `protobuf:"bytes,10,opt,name=disclaimer,proto3" json:"disclaimer,omitempty"`
- ArticleType string `protobuf:"bytes,11,opt,name=articleType,proto3" json:"articleType,omitempty"`
- SellerId string `protobuf:"bytes,19,opt,name=sellerId,proto3" json:"sellerId,omitempty"`
- SellerName string `protobuf:"bytes,20,opt,name=sellerName,proto3" json:"sellerName,omitempty"`
- Country string `protobuf:"bytes,21,opt,name=country,proto3" json:"country,omitempty"`
- Outlet *string `protobuf:"bytes,12,opt,name=outlet,proto3,oneof" json:"outlet,omitempty"`
- StoreId *string `protobuf:"bytes,22,opt,name=storeId,proto3,oneof" json:"storeId,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *AddItem) Reset() {
- *x = AddItem{}
- mi := &file_messages_proto_msgTypes[2]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *AddItem) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*AddItem) ProtoMessage() {}
-
-func (x *AddItem) ProtoReflect() protoreflect.Message {
- mi := &file_messages_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 AddItem.ProtoReflect.Descriptor instead.
-func (*AddItem) Descriptor() ([]byte, []int) {
- return file_messages_proto_rawDescGZIP(), []int{2}
-}
-
-func (x *AddItem) GetItemId() int64 {
- if x != nil {
- return x.ItemId
- }
- return 0
-}
-
-func (x *AddItem) GetQuantity() int32 {
- if x != nil {
- return x.Quantity
- }
- return 0
-}
-
-func (x *AddItem) GetPrice() int64 {
- if x != nil {
- return x.Price
- }
- return 0
-}
-
-func (x *AddItem) GetOrgPrice() int64 {
- if x != nil {
- return x.OrgPrice
- }
- return 0
-}
-
-func (x *AddItem) GetSku() string {
- if x != nil {
- return x.Sku
- }
- return ""
-}
-
-func (x *AddItem) GetName() string {
- if x != nil {
- return x.Name
- }
- return ""
-}
-
-func (x *AddItem) GetImage() string {
- if x != nil {
- return x.Image
- }
- return ""
-}
-
-func (x *AddItem) GetStock() int32 {
- if x != nil {
- return x.Stock
- }
- return 0
-}
-
-func (x *AddItem) GetTax() int32 {
- if x != nil {
- return x.Tax
- }
- return 0
-}
-
-func (x *AddItem) GetBrand() string {
- if x != nil {
- return x.Brand
- }
- return ""
-}
-
-func (x *AddItem) GetCategory() string {
- if x != nil {
- return x.Category
- }
- return ""
-}
-
-func (x *AddItem) GetCategory2() string {
- if x != nil {
- return x.Category2
- }
- return ""
-}
-
-func (x *AddItem) GetCategory3() string {
- if x != nil {
- return x.Category3
- }
- return ""
-}
-
-func (x *AddItem) GetCategory4() string {
- if x != nil {
- return x.Category4
- }
- return ""
-}
-
-func (x *AddItem) GetCategory5() string {
- if x != nil {
- return x.Category5
- }
- return ""
-}
-
-func (x *AddItem) GetDisclaimer() string {
- if x != nil {
- return x.Disclaimer
- }
- return ""
-}
-
-func (x *AddItem) GetArticleType() string {
- if x != nil {
- return x.ArticleType
- }
- return ""
-}
-
-func (x *AddItem) GetSellerId() string {
- if x != nil {
- return x.SellerId
- }
- return ""
-}
-
-func (x *AddItem) GetSellerName() string {
- if x != nil {
- return x.SellerName
- }
- return ""
-}
-
-func (x *AddItem) GetCountry() string {
- if x != nil {
- return x.Country
- }
- return ""
-}
-
-func (x *AddItem) GetOutlet() string {
- if x != nil && x.Outlet != nil {
- return *x.Outlet
- }
- return ""
-}
-
-func (x *AddItem) GetStoreId() string {
- if x != nil && x.StoreId != nil {
- return *x.StoreId
- }
- return ""
-}
-
-type RemoveItem struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- Id int64 `protobuf:"varint,1,opt,name=Id,proto3" json:"Id,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *RemoveItem) Reset() {
- *x = RemoveItem{}
- mi := &file_messages_proto_msgTypes[3]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *RemoveItem) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*RemoveItem) ProtoMessage() {}
-
-func (x *RemoveItem) ProtoReflect() protoreflect.Message {
- mi := &file_messages_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 RemoveItem.ProtoReflect.Descriptor instead.
-func (*RemoveItem) Descriptor() ([]byte, []int) {
- return file_messages_proto_rawDescGZIP(), []int{3}
-}
-
-func (x *RemoveItem) GetId() int64 {
- if x != nil {
- return x.Id
- }
- return 0
-}
-
-type ChangeQuantity struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
- Quantity int32 `protobuf:"varint,2,opt,name=quantity,proto3" json:"quantity,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *ChangeQuantity) Reset() {
- *x = ChangeQuantity{}
- mi := &file_messages_proto_msgTypes[4]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *ChangeQuantity) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*ChangeQuantity) ProtoMessage() {}
-
-func (x *ChangeQuantity) ProtoReflect() protoreflect.Message {
- mi := &file_messages_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 ChangeQuantity.ProtoReflect.Descriptor instead.
-func (*ChangeQuantity) Descriptor() ([]byte, []int) {
- return file_messages_proto_rawDescGZIP(), []int{4}
-}
-
-func (x *ChangeQuantity) GetId() int64 {
- if x != nil {
- return x.Id
- }
- return 0
-}
-
-func (x *ChangeQuantity) GetQuantity() int32 {
- if x != nil {
- return x.Quantity
- }
- return 0
-}
-
-type SetDelivery struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- Provider string `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"`
- Items []int64 `protobuf:"varint,2,rep,packed,name=items,proto3" json:"items,omitempty"`
- PickupPoint *PickupPoint `protobuf:"bytes,3,opt,name=pickupPoint,proto3,oneof" json:"pickupPoint,omitempty"`
- Country string `protobuf:"bytes,4,opt,name=country,proto3" json:"country,omitempty"`
- Zip string `protobuf:"bytes,5,opt,name=zip,proto3" json:"zip,omitempty"`
- Address *string `protobuf:"bytes,6,opt,name=address,proto3,oneof" json:"address,omitempty"`
- City *string `protobuf:"bytes,7,opt,name=city,proto3,oneof" json:"city,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *SetDelivery) Reset() {
- *x = SetDelivery{}
- mi := &file_messages_proto_msgTypes[5]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *SetDelivery) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*SetDelivery) ProtoMessage() {}
-
-func (x *SetDelivery) ProtoReflect() protoreflect.Message {
- mi := &file_messages_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 SetDelivery.ProtoReflect.Descriptor instead.
-func (*SetDelivery) Descriptor() ([]byte, []int) {
- return file_messages_proto_rawDescGZIP(), []int{5}
-}
-
-func (x *SetDelivery) GetProvider() string {
- if x != nil {
- return x.Provider
- }
- return ""
-}
-
-func (x *SetDelivery) GetItems() []int64 {
- if x != nil {
- return x.Items
- }
- return nil
-}
-
-func (x *SetDelivery) GetPickupPoint() *PickupPoint {
- if x != nil {
- return x.PickupPoint
- }
- return nil
-}
-
-func (x *SetDelivery) GetCountry() string {
- if x != nil {
- return x.Country
- }
- return ""
-}
-
-func (x *SetDelivery) GetZip() string {
- if x != nil {
- return x.Zip
- }
- return ""
-}
-
-func (x *SetDelivery) GetAddress() string {
- if x != nil && x.Address != nil {
- return *x.Address
- }
- return ""
-}
-
-func (x *SetDelivery) GetCity() string {
- if x != nil && x.City != nil {
- return *x.City
- }
- return ""
-}
-
-type SetPickupPoint struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- DeliveryId int64 `protobuf:"varint,1,opt,name=deliveryId,proto3" json:"deliveryId,omitempty"`
- Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"`
- Name *string `protobuf:"bytes,3,opt,name=name,proto3,oneof" json:"name,omitempty"`
- Address *string `protobuf:"bytes,4,opt,name=address,proto3,oneof" json:"address,omitempty"`
- City *string `protobuf:"bytes,5,opt,name=city,proto3,oneof" json:"city,omitempty"`
- Zip *string `protobuf:"bytes,6,opt,name=zip,proto3,oneof" json:"zip,omitempty"`
- Country *string `protobuf:"bytes,7,opt,name=country,proto3,oneof" json:"country,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *SetPickupPoint) Reset() {
- *x = SetPickupPoint{}
- mi := &file_messages_proto_msgTypes[6]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *SetPickupPoint) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*SetPickupPoint) ProtoMessage() {}
-
-func (x *SetPickupPoint) ProtoReflect() protoreflect.Message {
- mi := &file_messages_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 SetPickupPoint.ProtoReflect.Descriptor instead.
-func (*SetPickupPoint) Descriptor() ([]byte, []int) {
- return file_messages_proto_rawDescGZIP(), []int{6}
-}
-
-func (x *SetPickupPoint) GetDeliveryId() int64 {
- if x != nil {
- return x.DeliveryId
- }
- return 0
-}
-
-func (x *SetPickupPoint) GetId() string {
- if x != nil {
- return x.Id
- }
- return ""
-}
-
-func (x *SetPickupPoint) GetName() string {
- if x != nil && x.Name != nil {
- return *x.Name
- }
- return ""
-}
-
-func (x *SetPickupPoint) GetAddress() string {
- if x != nil && x.Address != nil {
- return *x.Address
- }
- return ""
-}
-
-func (x *SetPickupPoint) GetCity() string {
- if x != nil && x.City != nil {
- return *x.City
- }
- return ""
-}
-
-func (x *SetPickupPoint) GetZip() string {
- if x != nil && x.Zip != nil {
- return *x.Zip
- }
- return ""
-}
-
-func (x *SetPickupPoint) GetCountry() string {
- if x != nil && x.Country != nil {
- return *x.Country
- }
- return ""
-}
-
-type PickupPoint struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
- Name *string `protobuf:"bytes,2,opt,name=name,proto3,oneof" json:"name,omitempty"`
- Address *string `protobuf:"bytes,3,opt,name=address,proto3,oneof" json:"address,omitempty"`
- City *string `protobuf:"bytes,4,opt,name=city,proto3,oneof" json:"city,omitempty"`
- Zip *string `protobuf:"bytes,5,opt,name=zip,proto3,oneof" json:"zip,omitempty"`
- Country *string `protobuf:"bytes,6,opt,name=country,proto3,oneof" json:"country,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *PickupPoint) Reset() {
- *x = PickupPoint{}
- mi := &file_messages_proto_msgTypes[7]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *PickupPoint) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*PickupPoint) ProtoMessage() {}
-
-func (x *PickupPoint) ProtoReflect() protoreflect.Message {
- mi := &file_messages_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 PickupPoint.ProtoReflect.Descriptor instead.
-func (*PickupPoint) Descriptor() ([]byte, []int) {
- return file_messages_proto_rawDescGZIP(), []int{7}
-}
-
-func (x *PickupPoint) GetId() string {
- if x != nil {
- return x.Id
- }
- return ""
-}
-
-func (x *PickupPoint) GetName() string {
- if x != nil && x.Name != nil {
- return *x.Name
- }
- return ""
-}
-
-func (x *PickupPoint) GetAddress() string {
- if x != nil && x.Address != nil {
- return *x.Address
- }
- return ""
-}
-
-func (x *PickupPoint) GetCity() string {
- if x != nil && x.City != nil {
- return *x.City
- }
- return ""
-}
-
-func (x *PickupPoint) GetZip() string {
- if x != nil && x.Zip != nil {
- return *x.Zip
- }
- return ""
-}
-
-func (x *PickupPoint) GetCountry() string {
- if x != nil && x.Country != nil {
- return *x.Country
- }
- return ""
-}
-
-type RemoveDelivery struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *RemoveDelivery) Reset() {
- *x = RemoveDelivery{}
- mi := &file_messages_proto_msgTypes[8]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *RemoveDelivery) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*RemoveDelivery) ProtoMessage() {}
-
-func (x *RemoveDelivery) ProtoReflect() protoreflect.Message {
- mi := &file_messages_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 RemoveDelivery.ProtoReflect.Descriptor instead.
-func (*RemoveDelivery) Descriptor() ([]byte, []int) {
- return file_messages_proto_rawDescGZIP(), []int{8}
-}
-
-func (x *RemoveDelivery) GetId() int64 {
- if x != nil {
- return x.Id
- }
- return 0
-}
-
-type CreateCheckoutOrder struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- Terms string `protobuf:"bytes,1,opt,name=terms,proto3" json:"terms,omitempty"`
- Checkout string `protobuf:"bytes,2,opt,name=checkout,proto3" json:"checkout,omitempty"`
- Confirmation string `protobuf:"bytes,3,opt,name=confirmation,proto3" json:"confirmation,omitempty"`
- Push string `protobuf:"bytes,4,opt,name=push,proto3" json:"push,omitempty"`
- Validation string `protobuf:"bytes,5,opt,name=validation,proto3" json:"validation,omitempty"`
- Country string `protobuf:"bytes,6,opt,name=country,proto3" json:"country,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *CreateCheckoutOrder) Reset() {
- *x = CreateCheckoutOrder{}
- mi := &file_messages_proto_msgTypes[9]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *CreateCheckoutOrder) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*CreateCheckoutOrder) ProtoMessage() {}
-
-func (x *CreateCheckoutOrder) ProtoReflect() protoreflect.Message {
- mi := &file_messages_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 CreateCheckoutOrder.ProtoReflect.Descriptor instead.
-func (*CreateCheckoutOrder) Descriptor() ([]byte, []int) {
- return file_messages_proto_rawDescGZIP(), []int{9}
-}
-
-func (x *CreateCheckoutOrder) GetTerms() string {
- if x != nil {
- return x.Terms
- }
- return ""
-}
-
-func (x *CreateCheckoutOrder) GetCheckout() string {
- if x != nil {
- return x.Checkout
- }
- return ""
-}
-
-func (x *CreateCheckoutOrder) GetConfirmation() string {
- if x != nil {
- return x.Confirmation
- }
- return ""
-}
-
-func (x *CreateCheckoutOrder) GetPush() string {
- if x != nil {
- return x.Push
- }
- return ""
-}
-
-func (x *CreateCheckoutOrder) GetValidation() string {
- if x != nil {
- return x.Validation
- }
- return ""
-}
-
-func (x *CreateCheckoutOrder) GetCountry() string {
- if x != nil {
- return x.Country
- }
- return ""
-}
-
-type OrderCreated struct {
- state protoimpl.MessageState `protogen:"open.v1"`
- OrderId string `protobuf:"bytes,1,opt,name=orderId,proto3" json:"orderId,omitempty"`
- Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
-}
-
-func (x *OrderCreated) Reset() {
- *x = OrderCreated{}
- mi := &file_messages_proto_msgTypes[10]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *OrderCreated) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*OrderCreated) ProtoMessage() {}
-
-func (x *OrderCreated) ProtoReflect() protoreflect.Message {
- mi := &file_messages_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 OrderCreated.ProtoReflect.Descriptor instead.
-func (*OrderCreated) Descriptor() ([]byte, []int) {
- return file_messages_proto_rawDescGZIP(), []int{10}
-}
-
-func (x *OrderCreated) GetOrderId() string {
- if x != nil {
- return x.OrderId
- }
- return ""
-}
-
-func (x *OrderCreated) GetStatus() string {
- if x != nil {
- return x.Status
- }
- return ""
-}
-
-var File_messages_proto protoreflect.FileDescriptor
-
-const file_messages_proto_rawDesc = "" +
- "\n" +
- "\x0emessages.proto\x12\bmessages\"\x7f\n" +
- "\n" +
- "AddRequest\x12\x1a\n" +
- "\bquantity\x18\x01 \x01(\x05R\bquantity\x12\x10\n" +
- "\x03sku\x18\x02 \x01(\tR\x03sku\x12\x18\n" +
- "\acountry\x18\x03 \x01(\tR\acountry\x12\x1d\n" +
- "\astoreId\x18\x04 \x01(\tH\x00R\astoreId\x88\x01\x01B\n" +
- "\n" +
- "\b_storeId\"<\n" +
- "\x0eSetCartRequest\x12*\n" +
- "\x05items\x18\x01 \x03(\v2\x14.messages.AddRequestR\x05items\"\xe9\x04\n" +
- "\aAddItem\x12\x17\n" +
- "\aitem_id\x18\x01 \x01(\x03R\x06itemId\x12\x1a\n" +
- "\bquantity\x18\x02 \x01(\x05R\bquantity\x12\x14\n" +
- "\x05price\x18\x03 \x01(\x03R\x05price\x12\x1a\n" +
- "\borgPrice\x18\t \x01(\x03R\borgPrice\x12\x10\n" +
- "\x03sku\x18\x04 \x01(\tR\x03sku\x12\x12\n" +
- "\x04name\x18\x05 \x01(\tR\x04name\x12\x14\n" +
- "\x05image\x18\x06 \x01(\tR\x05image\x12\x14\n" +
- "\x05stock\x18\a \x01(\x05R\x05stock\x12\x10\n" +
- "\x03tax\x18\b \x01(\x05R\x03tax\x12\x14\n" +
- "\x05brand\x18\r \x01(\tR\x05brand\x12\x1a\n" +
- "\bcategory\x18\x0e \x01(\tR\bcategory\x12\x1c\n" +
- "\tcategory2\x18\x0f \x01(\tR\tcategory2\x12\x1c\n" +
- "\tcategory3\x18\x10 \x01(\tR\tcategory3\x12\x1c\n" +
- "\tcategory4\x18\x11 \x01(\tR\tcategory4\x12\x1c\n" +
- "\tcategory5\x18\x12 \x01(\tR\tcategory5\x12\x1e\n" +
- "\n" +
- "disclaimer\x18\n" +
- " \x01(\tR\n" +
- "disclaimer\x12 \n" +
- "\varticleType\x18\v \x01(\tR\varticleType\x12\x1a\n" +
- "\bsellerId\x18\x13 \x01(\tR\bsellerId\x12\x1e\n" +
- "\n" +
- "sellerName\x18\x14 \x01(\tR\n" +
- "sellerName\x12\x18\n" +
- "\acountry\x18\x15 \x01(\tR\acountry\x12\x1b\n" +
- "\x06outlet\x18\f \x01(\tH\x00R\x06outlet\x88\x01\x01\x12\x1d\n" +
- "\astoreId\x18\x16 \x01(\tH\x01R\astoreId\x88\x01\x01B\t\n" +
- "\a_outletB\n" +
- "\n" +
- "\b_storeId\"\x1c\n" +
- "\n" +
- "RemoveItem\x12\x0e\n" +
- "\x02Id\x18\x01 \x01(\x03R\x02Id\"<\n" +
- "\x0eChangeQuantity\x12\x0e\n" +
- "\x02id\x18\x01 \x01(\x03R\x02id\x12\x1a\n" +
- "\bquantity\x18\x02 \x01(\x05R\bquantity\"\x86\x02\n" +
- "\vSetDelivery\x12\x1a\n" +
- "\bprovider\x18\x01 \x01(\tR\bprovider\x12\x14\n" +
- "\x05items\x18\x02 \x03(\x03R\x05items\x12<\n" +
- "\vpickupPoint\x18\x03 \x01(\v2\x15.messages.PickupPointH\x00R\vpickupPoint\x88\x01\x01\x12\x18\n" +
- "\acountry\x18\x04 \x01(\tR\acountry\x12\x10\n" +
- "\x03zip\x18\x05 \x01(\tR\x03zip\x12\x1d\n" +
- "\aaddress\x18\x06 \x01(\tH\x01R\aaddress\x88\x01\x01\x12\x17\n" +
- "\x04city\x18\a \x01(\tH\x02R\x04city\x88\x01\x01B\x0e\n" +
- "\f_pickupPointB\n" +
- "\n" +
- "\b_addressB\a\n" +
- "\x05_city\"\xf9\x01\n" +
- "\x0eSetPickupPoint\x12\x1e\n" +
- "\n" +
- "deliveryId\x18\x01 \x01(\x03R\n" +
- "deliveryId\x12\x0e\n" +
- "\x02id\x18\x02 \x01(\tR\x02id\x12\x17\n" +
- "\x04name\x18\x03 \x01(\tH\x00R\x04name\x88\x01\x01\x12\x1d\n" +
- "\aaddress\x18\x04 \x01(\tH\x01R\aaddress\x88\x01\x01\x12\x17\n" +
- "\x04city\x18\x05 \x01(\tH\x02R\x04city\x88\x01\x01\x12\x15\n" +
- "\x03zip\x18\x06 \x01(\tH\x03R\x03zip\x88\x01\x01\x12\x1d\n" +
- "\acountry\x18\a \x01(\tH\x04R\acountry\x88\x01\x01B\a\n" +
- "\x05_nameB\n" +
- "\n" +
- "\b_addressB\a\n" +
- "\x05_cityB\x06\n" +
- "\x04_zipB\n" +
- "\n" +
- "\b_country\"\xd6\x01\n" +
- "\vPickupPoint\x12\x0e\n" +
- "\x02id\x18\x01 \x01(\tR\x02id\x12\x17\n" +
- "\x04name\x18\x02 \x01(\tH\x00R\x04name\x88\x01\x01\x12\x1d\n" +
- "\aaddress\x18\x03 \x01(\tH\x01R\aaddress\x88\x01\x01\x12\x17\n" +
- "\x04city\x18\x04 \x01(\tH\x02R\x04city\x88\x01\x01\x12\x15\n" +
- "\x03zip\x18\x05 \x01(\tH\x03R\x03zip\x88\x01\x01\x12\x1d\n" +
- "\acountry\x18\x06 \x01(\tH\x04R\acountry\x88\x01\x01B\a\n" +
- "\x05_nameB\n" +
- "\n" +
- "\b_addressB\a\n" +
- "\x05_cityB\x06\n" +
- "\x04_zipB\n" +
- "\n" +
- "\b_country\" \n" +
- "\x0eRemoveDelivery\x12\x0e\n" +
- "\x02id\x18\x01 \x01(\x03R\x02id\"\xb9\x01\n" +
- "\x13CreateCheckoutOrder\x12\x14\n" +
- "\x05terms\x18\x01 \x01(\tR\x05terms\x12\x1a\n" +
- "\bcheckout\x18\x02 \x01(\tR\bcheckout\x12\"\n" +
- "\fconfirmation\x18\x03 \x01(\tR\fconfirmation\x12\x12\n" +
- "\x04push\x18\x04 \x01(\tR\x04push\x12\x1e\n" +
- "\n" +
- "validation\x18\x05 \x01(\tR\n" +
- "validation\x12\x18\n" +
- "\acountry\x18\x06 \x01(\tR\acountry\"@\n" +
- "\fOrderCreated\x12\x18\n" +
- "\aorderId\x18\x01 \x01(\tR\aorderId\x12\x16\n" +
- "\x06status\x18\x02 \x01(\tR\x06statusB\fZ\n" +
- ".;messagesb\x06proto3"
-
-var (
- file_messages_proto_rawDescOnce sync.Once
- file_messages_proto_rawDescData []byte
-)
-
-func file_messages_proto_rawDescGZIP() []byte {
- file_messages_proto_rawDescOnce.Do(func() {
- file_messages_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_messages_proto_rawDesc), len(file_messages_proto_rawDesc)))
- })
- return file_messages_proto_rawDescData
-}
-
-var file_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
-var file_messages_proto_goTypes = []any{
- (*AddRequest)(nil), // 0: messages.AddRequest
- (*SetCartRequest)(nil), // 1: messages.SetCartRequest
- (*AddItem)(nil), // 2: messages.AddItem
- (*RemoveItem)(nil), // 3: messages.RemoveItem
- (*ChangeQuantity)(nil), // 4: messages.ChangeQuantity
- (*SetDelivery)(nil), // 5: messages.SetDelivery
- (*SetPickupPoint)(nil), // 6: messages.SetPickupPoint
- (*PickupPoint)(nil), // 7: messages.PickupPoint
- (*RemoveDelivery)(nil), // 8: messages.RemoveDelivery
- (*CreateCheckoutOrder)(nil), // 9: messages.CreateCheckoutOrder
- (*OrderCreated)(nil), // 10: messages.OrderCreated
-}
-var file_messages_proto_depIdxs = []int32{
- 0, // 0: messages.SetCartRequest.items:type_name -> messages.AddRequest
- 7, // 1: messages.SetDelivery.pickupPoint:type_name -> messages.PickupPoint
- 2, // [2:2] is the sub-list for method output_type
- 2, // [2:2] is the sub-list for method input_type
- 2, // [2:2] is the sub-list for extension type_name
- 2, // [2:2] is the sub-list for extension extendee
- 0, // [0:2] is the sub-list for field type_name
-}
-
-func init() { file_messages_proto_init() }
-func file_messages_proto_init() {
- if File_messages_proto != nil {
- return
- }
- file_messages_proto_msgTypes[0].OneofWrappers = []any{}
- file_messages_proto_msgTypes[2].OneofWrappers = []any{}
- file_messages_proto_msgTypes[5].OneofWrappers = []any{}
- file_messages_proto_msgTypes[6].OneofWrappers = []any{}
- file_messages_proto_msgTypes[7].OneofWrappers = []any{}
- type x struct{}
- out := protoimpl.TypeBuilder{
- File: protoimpl.DescBuilder{
- GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
- RawDescriptor: unsafe.Slice(unsafe.StringData(file_messages_proto_rawDesc), len(file_messages_proto_rawDesc)),
- NumEnums: 0,
- NumMessages: 11,
- NumExtensions: 0,
- NumServices: 0,
- },
- GoTypes: file_messages_proto_goTypes,
- DependencyIndexes: file_messages_proto_depIdxs,
- MessageInfos: file_messages_proto_msgTypes,
- }.Build()
- File_messages_proto = out.File
- file_messages_proto_goTypes = nil
- file_messages_proto_depIdxs = nil
-}
diff --git a/proto/messages.proto b/proto/messages.proto
index 9d50ae6..90f1008 100644
--- a/proto/messages.proto
+++ b/proto/messages.proto
@@ -1,6 +1,6 @@
syntax = "proto3";
package messages;
-option go_package = ".;messages";
+option go_package = "git.tornberg.me/go-cart-actor/proto;messages";
message AddRequest {
int32 quantity = 1;
diff --git a/remote_grain_grpc.go b/remote_grain_grpc.go
index c2237eb..81d7492 100644
--- a/remote_grain_grpc.go
+++ b/remote_grain_grpc.go
@@ -1,7 +1,6 @@
package main
import (
- "bytes"
"context"
"fmt"
"log"
@@ -68,69 +67,209 @@ func (g *RemoteGrainGRPC) GetId() CartId {
return g.Id
}
-// HandleMessage serializes the underlying mutation proto (without legacy message header)
-// and invokes the CartActor.Mutate RPC. It wraps the reply into a FrameWithPayload
-// for compatibility with existing higher-level code paths.
-func (g *RemoteGrainGRPC) HandleMessage(message *Message, isReplay bool) (*FrameWithPayload, error) {
- if message == nil {
- return nil, fmt.Errorf("nil message")
- }
+// Apply executes a cart mutation remotely using the typed oneof MutationEnvelope
+// and returns a *CartGrain reconstructed from the typed MutationReply (state oneof).
+func (g *RemoteGrainGRPC) Apply(content interface{}, isReplay bool) (*CartGrain, error) {
if isReplay {
- // Remote replay not expected; ignore to keep parity with old implementation.
return nil, fmt.Errorf("replay not supported for remote grains")
}
-
- handler, err := GetMessageHandler(message.Type)
- if err != nil {
- return nil, err
+ if content == nil {
+ return nil, fmt.Errorf("nil mutation content")
}
- // Ensure timestamp set (legacy behavior)
- if message.TimeStamp == nil {
- ts := time.Now().Unix()
- message.TimeStamp = &ts
- }
-
- // Marshal underlying proto payload only (no StorableMessageHeader)
- var buf bytes.Buffer
- err = handler.Write(message, &buf)
- if err != nil {
- return nil, fmt.Errorf("encode mutation payload: %w", err)
- }
-
- req := &proto.MutationRequest{
+ ts := time.Now().Unix()
+ env := &proto.MutationEnvelope{
CartId: g.Id.String(),
- Type: proto.MutationType(message.Type), // numeric mapping preserved
- Payload: buf.Bytes(),
- ClientTimestamp: *message.TimeStamp,
+ ClientTimestamp: ts,
+ }
+
+ switch m := content.(type) {
+ case *proto.AddRequest:
+ env.Mutation = &proto.MutationEnvelope_AddRequest{AddRequest: m}
+ case *proto.AddItem:
+ env.Mutation = &proto.MutationEnvelope_AddItem{AddItem: m}
+ case *proto.RemoveItem:
+ env.Mutation = &proto.MutationEnvelope_RemoveItem{RemoveItem: m}
+ case *proto.RemoveDelivery:
+ env.Mutation = &proto.MutationEnvelope_RemoveDelivery{RemoveDelivery: m}
+ case *proto.ChangeQuantity:
+ env.Mutation = &proto.MutationEnvelope_ChangeQuantity{ChangeQuantity: m}
+ case *proto.SetDelivery:
+ env.Mutation = &proto.MutationEnvelope_SetDelivery{SetDelivery: m}
+ case *proto.SetPickupPoint:
+ env.Mutation = &proto.MutationEnvelope_SetPickupPoint{SetPickupPoint: m}
+ case *proto.CreateCheckoutOrder:
+ env.Mutation = &proto.MutationEnvelope_CreateCheckoutOrder{CreateCheckoutOrder: m}
+ case *proto.SetCartRequest:
+ env.Mutation = &proto.MutationEnvelope_SetCartItems{SetCartItems: m}
+ case *proto.OrderCreated:
+ env.Mutation = &proto.MutationEnvelope_OrderCompleted{OrderCompleted: m}
+ default:
+ return nil, fmt.Errorf("unsupported mutation type %T", content)
}
ctx, cancel := context.WithTimeout(context.Background(), g.mutateTimeout)
defer cancel()
- resp, err := g.client.Mutate(ctx, req)
+ resp, err := g.client.Mutate(ctx, env)
if err != nil {
return nil, err
}
- frame := MakeFrameWithPayload(RemoteHandleMutationReply, StatusCode(resp.StatusCode), resp.Payload)
- return &frame, nil
+ // Map typed reply
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ if e := resp.GetError(); e != "" {
+ return nil, fmt.Errorf("remote mutation failed %d: %s", resp.StatusCode, e)
+ }
+ return nil, fmt.Errorf("remote mutation failed %d", resp.StatusCode)
+ }
+ state := resp.GetState()
+ if state == nil {
+ return nil, fmt.Errorf("mutation reply missing state on success")
+ }
+ // Reconstruct a lightweight CartGrain (only fields we expose internally)
+ grain := &CartGrain{
+ Id: ToCartId(state.CartId),
+ TotalPrice: state.TotalPrice,
+ TotalTax: state.TotalTax,
+ TotalDiscount: state.TotalDiscount,
+ PaymentInProgress: state.PaymentInProgress,
+ OrderReference: state.OrderReference,
+ PaymentStatus: state.PaymentStatus,
+ }
+ // Items
+ for _, it := range state.Items {
+ if it == nil {
+ continue
+ }
+ outlet := toPtr(it.Outlet)
+ storeId := toPtr(it.StoreId)
+ grain.Items = append(grain.Items, &CartItem{
+ Id: int(it.Id),
+ ItemId: int(it.SourceItemId),
+ Sku: it.Sku,
+ Name: it.Name,
+ Price: it.UnitPrice,
+ Quantity: int(it.Quantity),
+ TotalPrice: it.TotalPrice,
+ TotalTax: it.TotalTax,
+ OrgPrice: it.OrgPrice,
+ TaxRate: int(it.TaxRate),
+ Brand: it.Brand,
+ Category: it.Category,
+ Category2: it.Category2,
+ Category3: it.Category3,
+ Category4: it.Category4,
+ Category5: it.Category5,
+ Image: it.Image,
+ ArticleType: it.ArticleType,
+ SellerId: it.SellerId,
+ SellerName: it.SellerName,
+ Disclaimer: it.Disclaimer,
+ Outlet: outlet,
+ StoreId: storeId,
+ Stock: StockStatus(it.Stock),
+ })
+ }
+ // Deliveries
+ for _, d := range state.Deliveries {
+ if d == nil {
+ continue
+ }
+ intIds := make([]int, 0, len(d.ItemIds))
+ for _, id := range d.ItemIds {
+ intIds = append(intIds, int(id))
+ }
+ grain.Deliveries = append(grain.Deliveries, &CartDelivery{
+ Id: int(d.Id),
+ Provider: d.Provider,
+ Price: d.Price,
+ Items: intIds,
+ PickupPoint: d.PickupPoint,
+ })
+ }
+
+ return grain, nil
}
-// GetCurrentState calls CartActor.GetState and returns a FrameWithPayload
-// shaped like the legacy RemoteGetStateReply.
-func (g *RemoteGrainGRPC) GetCurrentState() (*FrameWithPayload, error) {
+// GetCurrentState retrieves the current cart state using the typed StateReply oneof.
+func (g *RemoteGrainGRPC) GetCurrentState() (*CartGrain, error) {
ctx, cancel := context.WithTimeout(context.Background(), g.stateTimeout)
defer cancel()
-
- resp, err := g.client.GetState(ctx, &proto.StateRequest{
- CartId: g.Id.String(),
- })
+ resp, err := g.client.GetState(ctx, &proto.StateRequest{CartId: g.Id.String()})
if err != nil {
return nil, err
}
- frame := MakeFrameWithPayload(RemoteGetStateReply, StatusCode(resp.StatusCode), resp.Payload)
- return &frame, nil
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ if e := resp.GetError(); e != "" {
+ return nil, fmt.Errorf("remote get state failed %d: %s", resp.StatusCode, e)
+ }
+ return nil, fmt.Errorf("remote get state failed %d", resp.StatusCode)
+ }
+ state := resp.GetState()
+ if state == nil {
+ return nil, fmt.Errorf("state reply missing state on success")
+ }
+ grain := &CartGrain{
+ Id: ToCartId(state.CartId),
+ TotalPrice: state.TotalPrice,
+ TotalTax: state.TotalTax,
+ TotalDiscount: state.TotalDiscount,
+ PaymentInProgress: state.PaymentInProgress,
+ OrderReference: state.OrderReference,
+ PaymentStatus: state.PaymentStatus,
+ }
+ for _, it := range state.Items {
+ if it == nil {
+ continue
+ }
+ outlet := toPtr(it.Outlet)
+ storeId := toPtr(it.StoreId)
+ grain.Items = append(grain.Items, &CartItem{
+ Id: int(it.Id),
+ ItemId: int(it.SourceItemId),
+ Sku: it.Sku,
+ Name: it.Name,
+ Price: it.UnitPrice,
+ Quantity: int(it.Quantity),
+ TotalPrice: it.TotalPrice,
+ TotalTax: it.TotalTax,
+ OrgPrice: it.OrgPrice,
+ TaxRate: int(it.TaxRate),
+ Brand: it.Brand,
+ Category: it.Category,
+ Category2: it.Category2,
+ Category3: it.Category3,
+ Category4: it.Category4,
+ Category5: it.Category5,
+ Image: it.Image,
+ ArticleType: it.ArticleType,
+ SellerId: it.SellerId,
+ SellerName: it.SellerName,
+ Disclaimer: it.Disclaimer,
+ Outlet: outlet,
+ StoreId: storeId,
+ Stock: StockStatus(it.Stock),
+ })
+ }
+ for _, d := range state.Deliveries {
+ if d == nil {
+ continue
+ }
+ intIds := make([]int, 0, len(d.ItemIds))
+ for _, id := range d.ItemIds {
+ intIds = append(intIds, int(id))
+ }
+ grain.Deliveries = append(grain.Deliveries, &CartDelivery{
+ Id: int(d.Id),
+ Provider: d.Provider,
+ Price: d.Price,
+ Items: intIds,
+ PickupPoint: d.PickupPoint,
+ })
+ }
+
+ return grain, nil
}
// Close closes the underlying gRPC connection if this adapter created it.
diff --git a/synced-pool.go b/synced-pool.go
index 455acc4..dfb7494 100644
--- a/synced-pool.go
+++ b/synced-pool.go
@@ -427,14 +427,14 @@ func (p *SyncedPool) getGrain(id CartId) (Grain, error) {
}
// Process applies mutation(s) to a grain (local or remote).
-func (p *SyncedPool) Process(id CartId, messages ...Message) (*FrameWithPayload, error) {
+func (p *SyncedPool) Process(id CartId, mutations ...interface{}) (*CartGrain, error) {
grain, err := p.getGrain(id)
if err != nil {
return nil, err
}
- var res *FrameWithPayload
- for _, m := range messages {
- res, err = grain.HandleMessage(&m, false)
+ var res *CartGrain
+ for _, m := range mutations {
+ res, err = grain.Apply(m, false)
if err != nil {
return nil, err
}
@@ -443,7 +443,7 @@ func (p *SyncedPool) Process(id CartId, messages ...Message) (*FrameWithPayload,
}
// Get returns current state of a grain (local or remote).
-func (p *SyncedPool) Get(id CartId) (*FrameWithPayload, error) {
+func (p *SyncedPool) Get(id CartId) (*CartGrain, error) {
grain, err := p.getGrain(id)
if err != nil {
return nil, err