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