diff --git a/README.md b/README.md
index 6db9855..67521f7 100644
--- a/README.md
+++ b/README.md
@@ -175,4 +175,240 @@ curl --cookie cookies.txt http://localhost:8080/cart/add/TEST-SKU-123
- Always regenerate protobuf Go code after modifying any `.proto` files (messages/cart_actor/control_plane)
- The generated `messages.pb.go` file should not be edited manually
-- Make sure your PATH includes the protoc-gen-go binary location (usually `$GOPATH/bin`)
\ No newline at end of file
+- Make sure your PATH includes the protoc-gen-go binary location (usually `$GOPATH/bin`)
+
+---
+
+## Architecture Overview
+
+The system is a distributed, sharded (by cart id) actor model implementation:
+
+- Each cart is a grain (an in‑memory struct `*CartGrain`) that owns and mutates its own state.
+- A **local grain pool** holds grains owned by the node.
+- A **synced (cluster) pool** (`SyncedPool`) coordinates multiple nodes and exposes local or remote grains through a uniform interface (`GrainPool`).
+- All inter‑node communication is gRPC:
+ - Cart mutation & state RPCs (CartActor service).
+ - Control plane RPCs (ControlPlane service) for membership, ownership negotiation, liveness, and graceful shutdown.
+
+### Key Processes
+
+1. Client HTTP request (or gRPC client) arrives with a cart identifier (cookie or path).
+2. The pool resolves ownership:
+ - If local grain exists → use it.
+ - If a remote host is known owner → a remote grain proxy (`RemoteGrainGRPC`) is used; it performs gRPC calls to the owning node.
+ - If ownership is unknown → node attempts to claim ownership (quorum negotiation) and spawns a local grain.
+3. Mutation is executed via the **mutation registry** (registry wraps domain logic + optional totals recomputation).
+4. Updated state returned to caller; ownership preserved unless relinquished later (not yet implemented to shed load).
+
+---
+
+## Grain & Mutation Model
+
+- `CartGrain` holds items, deliveries, pricing aggregates, and checkout/order metadata.
+- All mutations are registered via `RegisterMutation[T]` with signature:
+ ```
+ func(*CartGrain, *T) error
+ ```
+- `WithTotals()` flag triggers automatic recalculation of totals after successful handlers.
+- The old giant `switch` in `CartGrain.Apply` has been replaced by registry dispatch; unregistered mutations fail fast.
+- Adding a mutation:
+ 1. Define proto message.
+ 2. Generate code.
+ 3. Register handler (optionally WithTotals).
+ 4. Add gRPC RPC + request wrapper if the mutation must be remotely invokable.
+ 5. (Optional) Add HTTP endpoint mapping to the mutation.
+
+---
+
+## Local Grain Pool
+
+- Manages an in‑memory map `map[CartId]*CartGrain`.
+- Lazy spawn: first mutation or explicit access triggers `spawn(id)`.
+- TTL / purge loop periodically removes expired grains unless they changed recently (basic memory pressure management).
+- Capacity limit (`PoolSize`); oldest expired grain evicted first when full.
+
+---
+
+## Synced (Cluster) Pool
+
+`SyncedPool` wraps a local pool and tracks:
+
+- `remoteHosts`: known peer nodes (gRPC connections).
+- `remoteIndex`: mapping of cart id → remote grain proxy (`RemoteGrainGRPC`) for carts owned elsewhere.
+
+Responsibilities:
+
+1. Discovery integration (via a `Discovery` interface) adds/removes hosts.
+2. Periodic ping health checks (ControlPlane.Ping).
+3. Ownership negotiation:
+ - On first contention / unknown owner, node calls `ConfirmOwner` on peers to achieve quorum before making a local grain authoritative.
+4. Remote spawning:
+ - When a remote host reports its cart ids (`GetCartIds`), the pool creates remote proxies for fast routing.
+
+---
+
+## Remote Grain Proxies
+
+A `RemoteGrainGRPC` implements the `Grain` interface but delegates:
+
+- `Apply` → Specific CartActor per‑mutation RPC (e.g., `AddItem`, `RemoveItem`) constructed from the mutation type. (Legacy envelope removed.)
+- `GetCurrentState` → `CartActor.GetState`.
+
+Return path:
+
+1. gRPC reply (CartMutationReply / StateReply) → proto `CartState`.
+2. `ToCartState` / mapping reconstructs a local `CartGrain` snapshot for callers expecting grain semantics.
+
+---
+
+## Control Plane (Inter‑Node Coordination)
+
+Defined in `proto/control_plane.proto`:
+
+| RPC | Purpose |
+|-----|---------|
+| `Ping` | Liveness; increments missed ping counter if failing. |
+| `Negotiate` | Merges membership views; used after discovery events. |
+| `GetCartIds` | Enumerate locally owned carts for remote index seeding. |
+| `ConfirmOwner` | Quorum acknowledgment for ownership claim. |
+| `Closing` | Graceful shutdown notice; peers remove host & associated remote grains. |
+
+### Ownership / Quorum Rules
+
+- If total participating hosts < 3 → all must accept.
+- Otherwise majority acceptance (`ok >= total/2`).
+- On failure → local tentative grain is removed (rollback to avoid split‑brain).
+
+---
+
+## Request / Mutation Flow Examples
+
+### Local Mutation
+1. HTTP handler parses request → determines cart id.
+2. `SyncedPool.Apply`:
+ - Finds local grain (or spawns new after quorum).
+ - Executes registry mutation.
+3. Totals updated if flagged.
+4. HTTP response returns updated JSON (via `ToCartState`).
+
+### Remote Mutation
+1. `SyncedPool.Apply` sees cart mapped to a remote host.
+2. Routes to `RemoteGrainGRPC.Apply`.
+3. Remote node executes mutation locally and returns updated state over gRPC.
+4. Proxy materializes snapshot locally (not authoritative, read‑only view).
+
+### Checkout (Side‑Effecting, Non-Pure)
+- HTTP `/checkout` uses current grain snapshot to build payload (pure function).
+- Calls Klarna externally (not a mutation).
+- Applies `InitializeCheckout` mutation to persist reference + status.
+- Returns Klarna order JSON to client.
+
+---
+
+## Scaling & Deployment
+
+- **Horizontal scaling**: Add more nodes; discovery layer (Kubernetes / service registry) feeds hosts to `SyncedPool`.
+- **Sharding**: Implicit by cart id hash. Ownership is first-claim with quorum acceptance.
+- **Hot spots**: A single popular cart remains on one node; for heavy multi-client concurrency, future work could add read replicas or partitioning (not implemented).
+- **Capacity tuning**: Increase `PoolSize` & memory limits; adjust TTL for stale cart eviction.
+
+### Adding Nodes
+1. Node starts gRPC server (CartActor + ControlPlane).
+2. After brief delay, begins discovery watch; on event:
+ - New host → dial + negotiate → seed remote cart ids.
+3. Pings maintain health; failed hosts removed (proxies invalidated).
+
+---
+
+## Failure Handling
+
+| Scenario | Behavior |
+|----------|----------|
+| Remote host unreachable | Pings increment `MissedPings`; after threshold host removed. |
+| Ownership negotiation fails | Tentative local grain discarded. |
+| gRPC call error on remote mutation | Error bubbled to caller; no local fallback. |
+| Missing mutation registration | Fast failure with explicit error message. |
+| Partial checkout (Klarna fails) | No local state mutation for checkout; client sees error; cart remains unchanged. |
+
+---
+
+## Mutation Registry Summary
+
+- Central, type-safe registry prevents silent omission.
+- Each handler:
+ - Validates input.
+ - Mutates `*CartGrain`.
+ - Returns error for rejection.
+- Automatic totals recomputation reduces boilerplate and consistency risk.
+- Coverage test (add separately) can enforce all proto mutations are registered.
+
+---
+
+## gRPC Interfaces
+
+- **CartActor**: Per-mutation unary RPCs + `GetState`. (Checkout logic intentionally excluded; handled at HTTP layer.)
+- **ControlPlane**: Cluster coordination (Ping, Negotiate, ConfirmOwner, etc.).
+
+**Ports** (default / implied):
+- CartActor & ControlPlane share the same gRPC server/listener (single port, e.g. `:1337`).
+- Legacy frame/TCP code has been removed.
+
+---
+
+## Security & Future Enhancements
+
+| Area | Potential Improvement |
+|------|------------------------|
+| Transport Security | Add TLS / mTLS to gRPC servers & clients. |
+| Auth / RBAC | Intercept CartActor RPCs with auth metadata. |
+| Backpressure | Rate-limit remote mutation calls per host. |
+| Observability | Add per-mutation Prometheus metrics & tracing spans. |
+| Ownership | Add lease timeouts / fencing tokens for stricter guarantees. |
+| Batch Ops | Introduce batch mutation RPC or streaming updates (WatchState). |
+| Persistence | Reintroduce event log or snapshot persistence layer if durability required. |
+
+---
+
+## Adding a New Node (Operational Checklist)
+
+1. Deploy binary/container with same proto + registry.
+2. Expose gRPC port.
+3. Ensure discovery lists the new host.
+4. Node dials peers, negotiates membership.
+5. Remote cart proxies seeded.
+6. Traffic routed automatically based on ownership.
+
+---
+
+## Adding a New Mutation (Checklist Recap)
+
+1. Define proto message (+ request wrapper & RPC if remote invocation needed).
+2. Regenerate protobuf code.
+3. Implement & register handler (`RegisterMutation`).
+4. Add client (HTTP/gRPC) endpoint.
+5. Write unit + integration tests.
+6. (Optional) Add to coverage test list and docs.
+
+---
+
+## High-Level Data Flow Diagram (Text)
+
+```
+Client -> HTTP Handler -> SyncedPool -> (local?) -> Registry -> Grain State
+ \-> (remote?) -> RemoteGrainGRPC -> gRPC -> Remote CartActor -> Registry -> Grain
+ControlPlane: Discovery Events <-> Negotiation/Ping/ConfirmOwner <-> SyncedPool state
+```
+
+---
+
+## Troubleshooting
+
+| Symptom | Likely Cause | Action |
+|---------|--------------|--------|
+| New cart every request | Secure cookie over plain HTTP or not sending cookie jar | Disable Secure locally or use HTTPS & proper curl `-b` |
+| Unsupported mutation error | Missing registry handler | Add `RegisterMutation` for that proto |
+| Ownership flapping | Quorum failing due to intermittent peers | Investigate `ConfirmOwner` errors / network |
+| Remote mutation latency | Network / serialization overhead | Consider batching or colocating hot carts |
+| Checkout returns 500 | Klarna call failed | Inspect logs; no grain state mutated |
+
+---
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000..3ec9585
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,245 @@
+# TODO / Roadmap
+
+A living roadmap for improving the cart actor system. Focus areas:
+1. Reliability & correctness
+2. Simplicity of mutation & ownership flows
+3. Developer experience (DX)
+4. Operability (observability, tracing, metrics)
+5. Performance & scalability
+6. Security & multi-tenant readiness
+
+---
+
+## 1. Immediate Next Steps (High-Leverage)
+
+| Priority | Task | Goal | Effort | Owner | Notes |
+|----------|------|------|--------|-------|-------|
+| P0 | Add mutation registry coverage test | Ensure no unregistered mutations silently fail | S | | Failing fast in CI |
+| P0 | Add decodeJSON helper + 400 mapping for EOF | Reduce noisy 500 logs | S | | Improves client API clarity |
+| P0 | Regenerate protos & prune unused messages (CreateCheckoutOrder, Checkout RPC remnants) | Eliminate dead types | S | | Avoid confusion |
+| P0 | Add integration test: multi-node ownership negotiation | Validate quorum logic | M | | Spin up 2–3 nodes ephemeral |
+| P1 | Export Prometheus metrics for per-mutation counts & latency | Operability | M | | Wrap registry handlers |
+| P1 | Add graceful shutdown ordering (Closing → wait for acks → stop gRPC) | Reduce in-flight mutation failures | S | | Add context cancellation |
+| P1 | Add coverage for InitializeCheckout / OrderCreated flows | Checkout reliability | S | | Simulate Klarna stub |
+| P2 | Add optional batching client (apply multiple mutations locally then persist) | Performance | M | | Only if needed |
+
+---
+
+## 2. Simplification Opportunities
+
+### A. RemoteGrain Proxy Mapping
+Current: manual switch building each RPC call.
+Simplify by:
+- Generating a thin client adapter from proto RPC descriptors (codegen).
+- Or using a registry similar to mutation registry but for “outbound call constructors”.
+Benefit: adding a new mutation = add proto + register server handler + register outbound invoker (no switch edits).
+
+### B. Ownership Negotiation
+Current: ad hoc quorum rule in `SyncedPool`.
+Simplify:
+- Introduce explicit `OwnershipLease{holder, expiresAt, version}`.
+- Use monotonic version increment—reject stale ConfirmOwner replies.
+- Optional: add randomized backoff to reduce thundering herd on contested cart ids.
+
+### C. CartId Handling
+Current: ephemeral 16-byte array with trimmed string semantics.
+Simplify:
+- Use ULID / UUIDv7 (time-ordered, collision-resistant) for easier external correlation.
+- Provide helper `NewCartIdString()` and keep internal fixed-size if still desired.
+
+### D. Mutation Signatures
+Current: registry assumes `func(*CartGrain, *T) error`.
+Extension option: allow pure transforms returning a delta struct (for audit/logging):
+```
+type MutationResult struct {
+ Changed bool
+ Events []interface{}
+}
+```
+Only implement if auditing/event-sourcing reintroduced.
+
+---
+
+## 3. Developer Experience Improvements
+
+| Task | Rationale | Approach |
+|------|-----------|----------|
+| Makefile targets: `make run-single`, `make run-multi N=3` | Faster local cluster spin-up | Docker compose or background “mini cluster” scripts |
+| Template for new mutation (generator) | Reduce boilerplate | `go:generate` scanning proto for new RPCs |
+| Lint config (golangci-lint) | Catch subtle issues early | Add `.golangci.yml` |
+| Pre-commit hook for proto regeneration check | Avoid stale generated code | Script compares git diff after `make protogen` |
+| Example client (Go + curl snippets auto-generated) | Onboarding | Codegen a markdown from proto comments |
+
+---
+
+## 4. Observability / Metrics / Tracing
+
+| Area | Metric / Trace | Notes |
+|------|----------------|-------|
+| Mutation registry | `cart_mutations_total{type,success}`; duration histogram | Wrap handler |
+| Ownership negotiation | `cart_ownership_attempts_total{result}` | result=accepted,rejected,timeout |
+| Remote latency | `cart_remote_mutation_seconds{method}` | Use client interceptors |
+| Pings | `cart_remote_missed_pings_total{host}` | Already count, expose |
+| Checkout flow | `checkout_attempts_total`, `checkout_failures_total` | Differentiate Klarna vs internal errors |
+| Tracing | Span: HTTP handler → SyncedPool.Apply → (Remote?) gRPC → mutation handler | Add OpenTelemetry instrumentation |
+
+---
+
+## 5. Performance & Scalability
+
+| Concern | Idea | Trade-Off |
+|---------|------|-----------|
+| High mutation rate on single cart | Introduce optional mutation queue (serialize explicitly) | Slight latency increase per op |
+| Remote call overhead | Add client-side gRPC pooling & per-host circuit breaker | Complexity vs resilience |
+| TTL purge efficiency | Use min-heap or timing wheel instead of slice scan | More code, better big-N performance |
+| Batch network latency | Add `BatchMutate` RPC (list of mutations applied atomically) | Lost single-op simplicity |
+
+---
+
+## 6. Reliability Features
+
+| Feature | Description | Priority |
+|---------|-------------|----------|
+| Lease fencing token | Include `ownership_version` in all remote mutate requests | M |
+| Retry policy | Limited retry for transient network errors (idempotent mutations only) | L |
+| Dead host reconciliation | On host removal, proactively attempt re-acquire of its carts | M |
+| Drain mode | Node marks itself “draining” → refuses new ownership claims | M |
+
+---
+
+## 7. Security & Hardening
+
+| Area | Next Step | Detail |
+|------|-----------|--------|
+| Transport | mTLS on gRPC | Use SPIFFE IDs or simple CA |
+| AuthN/AuthZ | Interceptor enforcing service token | Inject metadata header |
+| Input validation | Strengthen JSON decode responses | Disallow unknown fields globally |
+| Rate limiting | Per-IP / per-cart throttling | Guard hotspot abuse |
+| Multi-tenancy | Tenant id dimension in cart id or metadata | Partition metrics & ownership |
+
+---
+
+## 8. Testing Strategy Enhancements
+
+| Gap | Improvement |
+|-----|------------|
+| No multi-node integration test in CI | Spin ephemeral in-process servers on randomized ports |
+| Mutation regression | Table-driven tests auto-discover handlers via registry |
+| Ownership race | Stress test: concurrent Apply on same new cart id from N goroutines |
+| Checkout external dependency | Klarna mock server (HTTptest) + deterministic responses |
+| Fuzzing | Fuzz `BuildCheckoutOrderPayload` & mutation handlers for panics |
+
+---
+
+## 9. Cleanup / Tech Debt
+
+| Item | Action |
+|------|--------|
+| Remove deprecated proto remnants (CreateCheckoutOrder, Checkout RPC) | Delete & regenerate |
+| Consolidate duplicate tax computations | Single helper with tax config |
+| Delivery price hard-coded (4900) | Config or pricing strategy interface |
+| Mixed naming (camel vs snake JSON historically) | Provide stable external API doc; accept old forms if needed |
+| Manual remote mutation switch (if still present) | Replace with generated outbound registry |
+| Mixed error responses (string bodies) | Standardize JSON: `{ "error": "...", "code": 400 }` |
+
+---
+
+## 10. Potential Future Features
+
+| Feature | Value | Complexity |
+|---------|-------|------------|
+| Streaming `WatchState` RPC | Real-time cart updates for clients | Medium |
+| Event sourcing / audit log | Replay, analytics, debugging | High |
+| Promotion / coupon engine plugin | Business extensibility | Medium |
+| Partial cart reservation / inventory lock | Stock accuracy under concurrency | High |
+| Multi-currency pricing | Globalization | Medium |
+| GraphQL facade | Client flexibility | Medium |
+
+---
+
+## 11. Suggested Prioritized Backlog (Condensed)
+
+1. Coverage test + decode error mapping (P0)
+2. Proto regeneration & cleanup (P0)
+3. Metrics wrapper for registry (P1)
+4. Multi-node ownership integration test (P1)
+5. Delivery pricing abstraction (P2)
+6. Lease version in remote RPCs (P2)
+7. BatchMutate evaluation (P3)
+8. TLS / auth hardening (P3) if going multi-tenant/public
+9. Event sourcing (Evaluate after stability) (P4)
+
+---
+
+## 12. Simplifying the Developer Workflow
+
+| Pain | Simplifier |
+|------|------------|
+| Manual mutation boilerplate | Code generator for registry stubs |
+| Forgetting totals | Enforce WithTotals lint: fail if mutation touches items/deliveries without flag |
+| Hard to inspect remote ownership | `/internal/ownership` debug endpoint (JSON of local + remoteIndex) |
+| Hard to see mutation timings | Add `?debug=latency` header to return per-mutation durations |
+| Cookie dev confusion (Secure flag) | Env var: `DEV_INSECURE_COOKIES=1` |
+
+---
+
+## 13. Example: Mutation Codegen Sketch (Future)
+
+Input: cart_actor.proto
+Output: `mutation_auto.go`
+- Detect messages used in RPC wrappers (e.g., `AddItemRequest` → payload field).
+- Generate `RegisterMutation` template if handler not found.
+- Mark with `// TODO implement logic`.
+
+---
+
+## 14. Risk / Impact Matrix (Abbreviated)
+
+| Change | Risk | Mitigation |
+|--------|------|-----------|
+| Replace remote switch with registry | Possible missing registration → runtime error | Coverage test gating CI |
+| Lease introduction | Split-brain if version mishandled | Increment + assert monotonic; test race |
+| BatchMutate | Large atomic operations starving others | Size limits & fair scheduling |
+| Event sourcing | Storage + replay complexity | Start with append-only log + compaction job |
+
+---
+
+## 15. Contributing Workflow (Proposed)
+
+1. Add / modify proto → run `make protogen`
+2. Implement mutation logic → add `RegisterMutation` invocation
+3. Add/Update tests (unit + integration)
+4. Run `make verify` (lint, test, coverage, proto diff)
+5. Open PR (template auto-checklist referencing this TODO)
+6. Merge requires green CI + coverage threshold
+
+---
+
+## 16. Open Questions
+
+| Question | Notes |
+|----------|-------|
+| Do we need sticky sessions for HTTP layer scaling? | Currently cart id routing suffices |
+| Should deliveries prune invalid line references on SetCartRequest? | Inconsistency risk; add optional cleanup |
+| Is checkout idempotency strict enough? | Multiple create vs update semantics |
+| Add version field to CartState for optimistic concurrency? | Could enable external CAS writes |
+
+---
+
+## 17. Tracking
+
+Mark any completed tasks with `[x]`:
+
+- [ ] Coverage test
+- [ ] Decode helper + 400 mapping
+- [ ] Proto cleanup
+- [ ] Registry metrics instrumentation
+- [ ] Ownership multi-node test
+- [ ] Lease versioning
+- [ ] Delivery pricing abstraction
+- [ ] TLS/mTLS internal
+- [ ] BatchMutate design doc
+
+---
+
+_Last updated: roadmap draft – refine after first metrics & scaling test run._
\ No newline at end of file
diff --git a/amqp-order-handler.go b/amqp-order-handler.go
new file mode 100644
index 0000000..5003732
--- /dev/null
+++ b/amqp-order-handler.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ amqp "github.com/rabbitmq/amqp091-go"
+)
+
+type AmqpOrderHandler struct {
+ Url string
+ Connection *amqp.Connection
+ Channel *amqp.Channel
+}
+
+func (h *AmqpOrderHandler) Connect() error {
+ conn, err := amqp.Dial(h.Url)
+ if err != nil {
+ return fmt.Errorf("failed to connect to RabbitMQ: %w", err)
+ }
+ h.Connection = conn
+
+ ch, err := conn.Channel()
+ if err != nil {
+ return fmt.Errorf("failed to open a channel: %w", err)
+ }
+ h.Channel = ch
+
+ return nil
+}
+
+func (h *AmqpOrderHandler) Close() error {
+ if h.Channel != nil {
+ h.Channel.Close()
+ }
+ if h.Connection != nil {
+ return h.Connection.Close()
+ }
+ return nil
+}
+
+func (h *AmqpOrderHandler) OrderCompleted(body []byte) error {
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ err := h.Channel.PublishWithContext(ctx,
+ "orders", // exchange
+ "new", // routing key
+ false, // mandatory
+ false, // immediate
+ amqp.Publishing{
+ ContentType: "application/json",
+ Body: body,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to publish a message: %w", err)
+ }
+
+ return nil
+}
diff --git a/cart-grain.go b/cart-grain.go
index ffca614..7f7dd99 100644
--- a/cart-grain.go
+++ b/cart-grain.go
@@ -1,11 +1,8 @@
package main
import (
- "bytes"
"encoding/json"
"fmt"
- "log"
- "slices"
"sync"
messages "git.tornberg.me/go-cart-actor/proto"
@@ -265,235 +262,14 @@ func GetTaxAmount(total int64, tax int) int64 {
func (c *CartGrain) Apply(content interface{}, isReplay bool) (*CartGrain, error) {
grainMutations.Inc()
- switch msg := content.(type) {
-
- case *messages.SetCartRequest:
- c.mu.Lock()
- 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)
+ updated, err := ApplyRegistered(c, content)
+ if err != nil {
+ if err == ErrMutationNotRegistered {
+ return nil, fmt.Errorf("unsupported mutation type %T (not registered)", content)
}
-
- 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)
+ return nil, err
}
-
- // (Optional) Append to new storage mechanism here if still required.
-
- return c, nil
+ return updated, nil
}
func (c *CartGrain) UpdateTotals() {
diff --git a/checkout_builder.go b/checkout_builder.go
new file mode 100644
index 0000000..c7bb53a
--- /dev/null
+++ b/checkout_builder.go
@@ -0,0 +1,119 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// CheckoutMeta carries the external / URL metadata required to build a
+// Klarna CheckoutOrder from a CartGrain snapshot. It deliberately excludes
+// any Klarna-specific response fields (HTML snippet, client token, etc.).
+type CheckoutMeta struct {
+ Terms string
+ Checkout string
+ Confirmation string
+ Validation string
+ Push string
+ Country string
+ Currency string // optional override (defaults to "SEK" if empty)
+ Locale string // optional override (defaults to "sv-se" if empty)
+}
+
+// BuildCheckoutOrderPayload converts the current cart grain + meta information
+// into a CheckoutOrder domain struct and returns its JSON-serialized payload
+// (to send to Klarna) alongside the structured CheckoutOrder object.
+//
+// This function is PURE: it does not perform any network I/O or mutate the
+// grain. The caller is responsible for:
+//
+// 1. Choosing whether to create or update the Klarna order.
+// 2. Invoking KlarnaClient.CreateOrder / UpdateOrder with the returned payload.
+// 3. Applying an InitializeCheckout mutation (or equivalent) with the
+// resulting Klarna order id + status.
+//
+// If you later need to support different tax rates per line, you can extend
+// CartItem / Delivery to expose that data and propagate it here.
+func BuildCheckoutOrderPayload(grain *CartGrain, meta *CheckoutMeta) ([]byte, *CheckoutOrder, error) {
+ if grain == nil {
+ return nil, nil, fmt.Errorf("nil grain")
+ }
+ if meta == nil {
+ return nil, nil, fmt.Errorf("nil checkout meta")
+ }
+
+ currency := meta.Currency
+ if currency == "" {
+ currency = "SEK"
+ }
+ locale := meta.Locale
+ if locale == "" {
+ locale = "sv-se"
+ }
+ country := meta.Country
+ if country == "" {
+ country = "SE" // sensible default; adjust if multi-country support changes
+ }
+
+ lines := make([]*Line, 0, len(grain.Items)+len(grain.Deliveries))
+
+ // Item lines
+ for _, it := range grain.Items {
+ if it == nil {
+ continue
+ }
+ lines = append(lines, &Line{
+ Type: "physical",
+ Reference: it.Sku,
+ Name: it.Name,
+ Quantity: it.Quantity,
+ UnitPrice: int(it.Price),
+ TaxRate: 2500, // TODO: derive if variable tax rates are introduced
+ QuantityUnit: "st",
+ TotalAmount: int(it.TotalPrice),
+ TotalTaxAmount: int(it.TotalTax),
+ ImageURL: fmt.Sprintf("https://www.elgiganten.se%s", it.Image),
+ })
+ }
+
+ // Delivery lines
+ for _, d := range grain.Deliveries {
+ if d == nil || d.Price <= 0 {
+ continue
+ }
+ lines = append(lines, &Line{
+ Type: "shipping_fee",
+ Reference: d.Provider,
+ Name: "Delivery",
+ Quantity: 1,
+ UnitPrice: int(d.Price),
+ TaxRate: 2500,
+ QuantityUnit: "st",
+ TotalAmount: int(d.Price),
+ TotalTaxAmount: int(GetTaxAmount(d.Price, 2500)),
+ })
+ }
+
+ order := &CheckoutOrder{
+ PurchaseCountry: country,
+ PurchaseCurrency: currency,
+ Locale: locale,
+ OrderAmount: int(grain.TotalPrice),
+ OrderTaxAmount: int(grain.TotalTax),
+ OrderLines: lines,
+ MerchantReference1: grain.Id.String(),
+ MerchantURLS: &CheckoutMerchantURLS{
+ Terms: meta.Terms,
+ Checkout: meta.Checkout,
+ Confirmation: meta.Confirmation,
+ Validation: meta.Validation,
+ Push: meta.Push,
+ },
+ }
+
+ payload, err := json.Marshal(order)
+ if err != nil {
+ return nil, nil, fmt.Errorf("marshal checkout order: %w", err)
+ }
+
+ return payload, order, nil
+}
diff --git a/cookies.txt b/cookies.txt
new file mode 100644
index 0000000..be2ec67
--- /dev/null
+++ b/cookies.txt
@@ -0,0 +1,5 @@
+# Netscape HTTP Cookie File
+# https://curl.se/docs/http-cookies.html
+# This file was generated by libcurl! Edit at your own risk.
+
+#HttpOnly_localhost FALSE / FALSE 1761304670 cartid 4393545184291837
diff --git a/go.mod b/go.mod
index 3baefef..0148cea 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ 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 ef3aab2..7c2ab17 100644
--- a/go.sum
+++ b/go.sum
@@ -70,10 +70,14 @@ 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=
@@ -100,6 +104,8 @@ 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 8391948..43cf88e 100644
--- a/grain-pool.go
+++ b/grain-pool.go
@@ -26,7 +26,7 @@ var (
)
type GrainPool interface {
- Process(id CartId, mutations ...interface{}) (*CartGrain, error)
+ Apply(id CartId, mutation interface{}) (*CartGrain, error)
Get(id CartId) (*CartGrain, error)
}
@@ -142,18 +142,12 @@ func (p *GrainLocalPool) GetGrain(id CartId) (*CartGrain, error) {
return grain, err
}
-func (p *GrainLocalPool) Process(id CartId, mutations ...interface{}) (*CartGrain, error) {
+func (p *GrainLocalPool) Apply(id CartId, mutation interface{}) (*CartGrain, error) {
grain, err := p.GetGrain(id)
- var result *CartGrain
- if err == nil && grain != nil {
- for _, m := range mutations {
- result, err = grain.Apply(m, false)
- if err != nil {
- break
- }
- }
+ if err != nil || grain == nil {
+ return nil, err
}
- return result, err
+ return grain.Apply(mutation, false)
}
func (p *GrainLocalPool) Get(id CartId) (*CartGrain, error) {
diff --git a/grpc_integration_test.go b/grpc_integration_test.go
index 3e54581..863bd7f 100644
--- a/grpc_integration_test.go
+++ b/grpc_integration_test.go
@@ -12,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 the oneof MutationEnvelope directly to avoid hitting external product
+// This test uses the new per-mutation AddItem RPC (breaking v2 API) to avoid external product fetch logic
// fetching logic (FetchItem) which would require network I/O.
func TestCartActorMutationAndState(t *testing.T) {
// Setup local grain pool + synced pool (no discovery, single host)
@@ -60,32 +60,29 @@ func TestCartActorMutationAndState(t *testing.T) {
Country: "se",
}
- // Build oneof envelope directly (no legacy handler/enum)
- envelope := &messages.MutationEnvelope{
+ // Issue AddItem RPC directly (breaking v2 API)
+ addResp, err := cartClient.AddItem(context.Background(), &messages.AddItemRequest{
CartId: cartID,
ClientTimestamp: time.Now().Unix(),
- Mutation: &messages.MutationEnvelope_AddItem{
- AddItem: addItem,
- },
- }
-
- // Issue Mutate RPC
- mutResp, err := cartClient.Mutate(context.Background(), envelope)
+ Payload: addItem,
+ })
if err != nil {
- t.Fatalf("Mutate RPC error: %v", err)
+ t.Fatalf("AddItem RPC error: %v", err)
}
- if mutResp.StatusCode != 200 {
- t.Fatalf("Mutate returned non-200 status: %d, error: %s", mutResp.StatusCode, mutResp.GetError())
+ if addResp.StatusCode != 200 {
+ t.Fatalf("AddItem returned non-200 status: %d, error: %s", addResp.StatusCode, addResp.GetError())
}
- // Validate the response state
- state := mutResp.GetState()
+ // Validate the response state (from AddItem)
+ state := addResp.GetState()
if state == nil {
- t.Fatalf("Mutate response state is nil")
+ t.Fatalf("AddItem response state is nil")
}
+ // (Removed obsolete Mutate response handling)
+
if len(state.Items) != 1 {
- t.Fatalf("Expected 1 item after mutation, got %d", len(state.Items))
+ t.Fatalf("Expected 1 item after AddItem, got %d", len(state.Items))
}
if state.Items[0].Sku != "test-sku" {
t.Fatalf("Unexpected item SKU: %s", state.Items[0].Sku)
diff --git a/grpc_server.go b/grpc_server.go
index c5b2a42..06c220f 100644
--- a/grpc_server.go
+++ b/grpc_server.go
@@ -5,6 +5,7 @@ import (
"fmt"
"log"
"net"
+ "time"
messages "git.tornberg.me/go-cart-actor/proto"
"google.golang.org/grpc"
@@ -17,7 +18,7 @@ type cartActorGRPCServer struct {
messages.UnimplementedCartActorServer
messages.UnimplementedControlPlaneServer
- pool GrainPool // For cart state mutations and queries
+ pool GrainPool // For cart state mutations and queries
syncedPool *SyncedPool // For cluster membership and control
}
@@ -29,62 +30,125 @@ func NewCartActorGRPCServer(pool GrainPool, syncedPool *SyncedPool) *cartActorGR
}
}
-// 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())
-
- 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
- }
-
- // 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)
+// applyMutation routes a single cart mutation to the target grain (used by per-mutation RPC handlers).
+func (s *cartActorGRPCServer) applyMutation(cartID string, mutation interface{}) *messages.CartMutationReply {
+ grain, err := s.pool.Apply(ToCartId(cartID), mutation)
if err != nil {
- return &messages.MutationReply{
- StatusCode: 500,
- Result: &messages.MutationReply_Error{Error: err.Error()},
+ return &messages.CartMutationReply{
+ StatusCode: 500,
+ Result: &messages.CartMutationReply_Error{Error: err.Error()},
+ ServerTimestamp: time.Now().Unix(),
+ }
+ }
+ cartState := ToCartState(grain)
+ return &messages.CartMutationReply{
+ StatusCode: 200,
+ Result: &messages.CartMutationReply_State{State: cartState},
+ ServerTimestamp: time.Now().Unix(),
+ }
+}
+
+func (s *cartActorGRPCServer) AddRequest(ctx context.Context, req *messages.AddRequestRequest) (*messages.CartMutationReply, error) {
+ if req.GetCartId() == "" {
+ return &messages.CartMutationReply{
+ StatusCode: 400,
+ Result: &messages.CartMutationReply_Error{Error: "cart_id is required"},
+ ServerTimestamp: time.Now().Unix(),
}, nil
}
+ return s.applyMutation(req.GetCartId(), req.GetPayload()), nil
+}
- // Map the internal grain state to the protobuf representation.
- cartState := ToCartState(grain)
+func (s *cartActorGRPCServer) AddItem(ctx context.Context, req *messages.AddItemRequest) (*messages.CartMutationReply, error) {
+ if req.GetCartId() == "" {
+ return &messages.CartMutationReply{
+ StatusCode: 400,
+ Result: &messages.CartMutationReply_Error{Error: "cart_id is required"},
+ ServerTimestamp: time.Now().Unix(),
+ }, nil
+ }
+ return s.applyMutation(req.GetCartId(), req.GetPayload()), nil
+}
- return &messages.MutationReply{
- StatusCode: 200,
- Result: &messages.MutationReply_State{State: cartState},
- }, nil
+func (s *cartActorGRPCServer) RemoveItem(ctx context.Context, req *messages.RemoveItemRequest) (*messages.CartMutationReply, error) {
+ if req.GetCartId() == "" {
+ return &messages.CartMutationReply{
+ StatusCode: 400,
+ Result: &messages.CartMutationReply_Error{Error: "cart_id is required"},
+ ServerTimestamp: time.Now().Unix(),
+ }, nil
+ }
+ return s.applyMutation(req.GetCartId(), req.GetPayload()), nil
+}
+
+func (s *cartActorGRPCServer) RemoveDelivery(ctx context.Context, req *messages.RemoveDeliveryRequest) (*messages.CartMutationReply, error) {
+ if req.GetCartId() == "" {
+ return &messages.CartMutationReply{
+ StatusCode: 400,
+ Result: &messages.CartMutationReply_Error{Error: "cart_id is required"},
+ ServerTimestamp: time.Now().Unix(),
+ }, nil
+ }
+ return s.applyMutation(req.GetCartId(), req.GetPayload()), nil
+}
+
+func (s *cartActorGRPCServer) ChangeQuantity(ctx context.Context, req *messages.ChangeQuantityRequest) (*messages.CartMutationReply, error) {
+ if req.GetCartId() == "" {
+ return &messages.CartMutationReply{
+ StatusCode: 400,
+ Result: &messages.CartMutationReply_Error{Error: "cart_id is required"},
+ ServerTimestamp: time.Now().Unix(),
+ }, nil
+ }
+ return s.applyMutation(req.GetCartId(), req.GetPayload()), nil
+}
+
+func (s *cartActorGRPCServer) SetDelivery(ctx context.Context, req *messages.SetDeliveryRequest) (*messages.CartMutationReply, error) {
+ if req.GetCartId() == "" {
+ return &messages.CartMutationReply{
+ StatusCode: 400,
+ Result: &messages.CartMutationReply_Error{Error: "cart_id is required"},
+ ServerTimestamp: time.Now().Unix(),
+ }, nil
+ }
+ return s.applyMutation(req.GetCartId(), req.GetPayload()), nil
+}
+
+func (s *cartActorGRPCServer) SetPickupPoint(ctx context.Context, req *messages.SetPickupPointRequest) (*messages.CartMutationReply, error) {
+ if req.GetCartId() == "" {
+ return &messages.CartMutationReply{
+ StatusCode: 400,
+ Result: &messages.CartMutationReply_Error{Error: "cart_id is required"},
+ ServerTimestamp: time.Now().Unix(),
+ }, nil
+ }
+ return s.applyMutation(req.GetCartId(), req.GetPayload()), nil
+}
+
+/*
+Checkout RPC removed. Checkout is handled at the HTTP layer (PoolServer.HandleCheckout).
+*/
+
+func (s *cartActorGRPCServer) SetCartItems(ctx context.Context, req *messages.SetCartItemsRequest) (*messages.CartMutationReply, error) {
+ if req.GetCartId() == "" {
+ return &messages.CartMutationReply{
+ StatusCode: 400,
+ Result: &messages.CartMutationReply_Error{Error: "cart_id is required"},
+ ServerTimestamp: time.Now().Unix(),
+ }, nil
+ }
+ return s.applyMutation(req.GetCartId(), req.GetPayload()), nil
+}
+
+func (s *cartActorGRPCServer) OrderCompleted(ctx context.Context, req *messages.OrderCompletedRequest) (*messages.CartMutationReply, error) {
+ if req.GetCartId() == "" {
+ return &messages.CartMutationReply{
+ StatusCode: 400,
+ Result: &messages.CartMutationReply_Error{Error: "cart_id is required"},
+ ServerTimestamp: time.Now().Unix(),
+ }, nil
+ }
+ return s.applyMutation(req.GetCartId(), req.GetPayload()), nil
}
// GetState retrieves the current state of a cart grain.
@@ -136,4 +200,4 @@ func StartGRPCServer(addr string, pool GrainPool, syncedPool *SyncedPool) (*grpc
}()
return grpcServer, nil
-}
\ No newline at end of file
+}
diff --git a/main.go b/main.go
index 1e32e7c..a5e33a3 100644
--- a/main.go
+++ b/main.go
@@ -1,15 +1,21 @@
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"
)
@@ -76,13 +82,60 @@ 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")
var amqpUrl = os.Getenv("AMQP_URL")
var KlarnaInstance = NewKlarnaClient(KlarnaPlaygroundUrl, os.Getenv("KLARNA_API_USERNAME"), os.Getenv("KLARNA_API_PASSWORD"))
+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 GetDiscovery() Discovery {
if podIp == "" {
return nil
@@ -100,8 +153,6 @@ func GetDiscovery() Discovery {
return NewK8sDiscovery(client)
}
-
-
func main() {
storage, err := NewDiskStorage(fmt.Sprintf("data/%s_state.gob", name))
@@ -134,7 +185,185 @@ 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)
+ _, err = syncedServer.pool.Apply(cartId, getCheckoutOrder(r.Host, cartId))
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ w.Write([]byte(err.Error()))
+ }
+ // v2: Apply now returns *CartGrain; order creation handled inside grain (no payload to unmarshal)
+ } 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)
@@ -148,7 +377,34 @@ func main() {
done <- true
}()
- log.Print("Server started at port 1337")
+ log.Print("Server started at port 8083")
+ go http.ListenAndServe(":8083", mux)
<-done
}
+
+func triggerOrderCompleted(err error, syncedServer *PoolServer, order *CheckoutOrder) error {
+ _, err = syncedServer.pool.Apply(ToCartId(order.MerchantReference1), &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/mutation_add_item.go b/mutation_add_item.go
new file mode 100644
index 0000000..6053d3d
--- /dev/null
+++ b/mutation_add_item.go
@@ -0,0 +1,82 @@
+package main
+
+import (
+ "fmt"
+
+ messages "git.tornberg.me/go-cart-actor/proto"
+)
+
+// mutation_add_item.go
+//
+// Registers the AddItem cart mutation in the generic mutation registry.
+// This replaces the legacy switch-based logic previously found in CartGrain.Apply.
+//
+// Behavior:
+// * Validates quantity > 0
+// * If an item with same SKU exists -> increases quantity
+// * Else creates a new CartItem with computed tax amounts
+// * Totals recalculated automatically via WithTotals()
+//
+// NOTE: Any future field additions in messages.AddItem that affect pricing / tax
+// must keep this handler in sync.
+
+func init() {
+ RegisterMutation[messages.AddItem](
+ "AddItem",
+ func(g *CartGrain, m *messages.AddItem) error {
+ if m == nil {
+ return fmt.Errorf("AddItem: nil payload")
+ }
+ if m.Quantity < 1 {
+ return fmt.Errorf("AddItem: invalid quantity %d", m.Quantity)
+ }
+
+ // Fast path: merge with existing item having same SKU
+ if existing, found := g.FindItemWithSku(m.Sku); found {
+ existing.Quantity += int(m.Quantity)
+ return nil
+ }
+
+ g.mu.Lock()
+ defer g.mu.Unlock()
+
+ g.lastItemId++
+ taxRate := 2500
+ if m.Tax > 0 {
+ taxRate = int(m.Tax)
+ }
+ taxAmountPerUnit := GetTaxAmount(m.Price, taxRate)
+
+ g.Items = append(g.Items, &CartItem{
+ Id: g.lastItemId,
+ ItemId: int(m.ItemId),
+ Quantity: int(m.Quantity),
+ Sku: m.Sku,
+ Name: m.Name,
+ Price: m.Price,
+ TotalPrice: m.Price * int64(m.Quantity),
+ TotalTax: int64(taxAmountPerUnit * int64(m.Quantity)),
+ Image: m.Image,
+ Stock: StockStatus(m.Stock),
+ Disclaimer: m.Disclaimer,
+ Brand: m.Brand,
+ Category: m.Category,
+ Category2: m.Category2,
+ Category3: m.Category3,
+ Category4: m.Category4,
+ Category5: m.Category5,
+ OrgPrice: m.OrgPrice,
+ ArticleType: m.ArticleType,
+ Outlet: m.Outlet,
+ SellerId: m.SellerId,
+ SellerName: m.SellerName,
+ Tax: int(taxAmountPerUnit),
+ TaxRate: taxRate,
+ StoreId: m.StoreId,
+ })
+
+ return nil
+ },
+ WithTotals(), // Recalculate totals after successful mutation
+ )
+}
diff --git a/mutation_add_request.go b/mutation_add_request.go
new file mode 100644
index 0000000..a4ad874
--- /dev/null
+++ b/mutation_add_request.go
@@ -0,0 +1,61 @@
+package main
+
+import (
+ "fmt"
+
+ messages "git.tornberg.me/go-cart-actor/proto"
+)
+
+// mutation_add_request.go
+//
+// Registers the AddRequest mutation. This mutation is a higher-level intent
+// (add by SKU + quantity) which may translate into either:
+// - Increasing quantity of an existing line (same SKU), OR
+// - Creating a new item by performing a product lookup (via getItemData inside CartGrain.AddItem)
+//
+// Behavior:
+// - Validates non-empty SKU and quantity > 0
+// - If an item with the SKU already exists: increments its quantity
+// - Else delegates to CartGrain.AddItem (which itself produces an AddItem mutation)
+// - Totals recalculated automatically (WithTotals)
+//
+// NOTE:
+// - This handler purposely avoids duplicating the detailed AddItem logic;
+// it reuses CartGrain.AddItem which then flows through the AddItem mutation
+// registry handler.
+// - Double total recalculation can occur (AddItem has WithTotals too), but
+// is acceptable for clarity. Optimize later if needed.
+//
+// Potential future improvements:
+// - Stock validation before increasing quantity
+// - Reservation logic or concurrency guards around stock updates
+// - Coupon / pricing rules applied conditionally during add-by-sku
+func init() {
+ RegisterMutation[messages.AddRequest](
+ "AddRequest",
+ func(g *CartGrain, m *messages.AddRequest) error {
+ if m == nil {
+ return fmt.Errorf("AddRequest: nil payload")
+ }
+ if m.Sku == "" {
+ return fmt.Errorf("AddRequest: sku is empty")
+ }
+ if m.Quantity < 1 {
+ return fmt.Errorf("AddRequest: invalid quantity %d", m.Quantity)
+ }
+
+ // Existing line: accumulate quantity only.
+ if existing, found := g.FindItemWithSku(m.Sku); found {
+ existing.Quantity += int(m.Quantity)
+ return nil
+ }
+
+ // New line: delegate to higher-level AddItem flow (product lookup).
+ // We intentionally ignore the returned *CartGrain; registry will
+ // do totals again after this handler returns (harmless).
+ _, err := g.AddItem(m.Sku, int(m.Quantity), m.Country, m.StoreId)
+ return err
+ },
+ WithTotals(),
+ )
+}
diff --git a/mutation_change_quantity.go b/mutation_change_quantity.go
new file mode 100644
index 0000000..d311ce9
--- /dev/null
+++ b/mutation_change_quantity.go
@@ -0,0 +1,58 @@
+package main
+
+import (
+ "fmt"
+
+ messages "git.tornberg.me/go-cart-actor/proto"
+)
+
+// mutation_change_quantity.go
+//
+// Registers the ChangeQuantity mutation.
+//
+// Behavior:
+// - Locates an item by its cart-local line item Id (not source item_id).
+// - If requested quantity <= 0 the line is removed.
+// - Otherwise the line's Quantity field is updated.
+// - Totals are recalculated (WithTotals).
+//
+// Error handling:
+// - Returns an error if the item Id is not found.
+// - Returns an error if payload is nil (defensive).
+//
+// Concurrency:
+// - Uses the grain's RW-safe mutation pattern: we mutate in place under
+// the grain's implicit expectation that higher layers control access.
+// (If strict locking is required around every mutation, wrap logic in
+// an explicit g.mu.Lock()/Unlock(), but current model mirrors prior code.)
+func init() {
+ RegisterMutation[messages.ChangeQuantity](
+ "ChangeQuantity",
+ func(g *CartGrain, m *messages.ChangeQuantity) error {
+ if m == nil {
+ return fmt.Errorf("ChangeQuantity: nil payload")
+ }
+
+ foundIndex := -1
+ for i, it := range g.Items {
+ if it.Id == int(m.Id) {
+ foundIndex = i
+ break
+ }
+ }
+ if foundIndex == -1 {
+ return fmt.Errorf("ChangeQuantity: item id %d not found", m.Id)
+ }
+
+ if m.Quantity <= 0 {
+ // Remove the item
+ g.Items = append(g.Items[:foundIndex], g.Items[foundIndex+1:]...)
+ return nil
+ }
+
+ g.Items[foundIndex].Quantity = int(m.Quantity)
+ return nil
+ },
+ WithTotals(),
+ )
+}
diff --git a/mutation_initialize_checkout.go b/mutation_initialize_checkout.go
new file mode 100644
index 0000000..f43c128
--- /dev/null
+++ b/mutation_initialize_checkout.go
@@ -0,0 +1,49 @@
+package main
+
+import (
+ "fmt"
+
+ messages "git.tornberg.me/go-cart-actor/proto"
+)
+
+// mutation_initialize_checkout.go
+//
+// Registers the InitializeCheckout mutation.
+// This mutation is invoked AFTER an external Klarna checkout session
+// has been successfully created or updated. It persists the Klarna
+// order reference / status and marks the cart as having a payment in progress.
+//
+// Behavior:
+// - Sets OrderReference to the Klarna order ID (overwriting if already set).
+// - Sets PaymentStatus to the current Klarna status.
+// - Sets / updates PaymentInProgress flag.
+// - Does NOT alter pricing or line items (so no totals recalculation).
+//
+// Validation:
+// - Returns an error if payload is nil.
+// - Returns an error if orderId is empty (integrity guard).
+//
+// Concurrency:
+// - Relies on upstream mutation serialization for a single grain. If
+// parallel checkout attempts are possible, add higher-level guards
+// (e.g. reject if PaymentInProgress already true unless reusing
+// the same OrderReference).
+func init() {
+ RegisterMutation[messages.InitializeCheckout](
+ "InitializeCheckout",
+ func(g *CartGrain, m *messages.InitializeCheckout) error {
+ if m == nil {
+ return fmt.Errorf("InitializeCheckout: nil payload")
+ }
+ if m.OrderId == "" {
+ return fmt.Errorf("InitializeCheckout: missing orderId")
+ }
+
+ g.OrderReference = m.OrderId
+ g.PaymentStatus = m.Status
+ g.PaymentInProgress = m.PaymentInProgress
+ return nil
+ },
+ // No WithTotals(): monetary aggregates are unaffected.
+ )
+}
diff --git a/mutation_order_created.go b/mutation_order_created.go
new file mode 100644
index 0000000..b83a2cb
--- /dev/null
+++ b/mutation_order_created.go
@@ -0,0 +1,53 @@
+package main
+
+import (
+ "fmt"
+
+ messages "git.tornberg.me/go-cart-actor/proto"
+)
+
+// mutation_order_created.go
+//
+// Registers the OrderCreated mutation.
+//
+// This mutation represents the completion (or state transition) of an order
+// initiated earlier via InitializeCheckout / external Klarna processing.
+// It finalizes (or updates) the cart's order metadata.
+//
+// Behavior:
+// - Validates payload non-nil and OrderId not empty.
+// - Sets (or overwrites) OrderReference with the provided OrderId.
+// - Sets PaymentStatus from payload.Status.
+// - Marks PaymentInProgress = false (checkout flow finished / acknowledged).
+// - Does NOT adjust monetary totals (no WithTotals()).
+//
+// Notes / Future Extensions:
+// - If multiple order completion events can arrive (e.g., retries / webhook
+// replays), this handler is idempotent: it simply overwrites fields.
+// - If you need to guard against conflicting order IDs, add a check:
+// if g.OrderReference != "" && g.OrderReference != m.OrderId { ... }
+// - Add audit logging or metrics here if required.
+//
+// Concurrency:
+// - Relies on the higher-level guarantee that Apply() calls are serialized
+// per grain. If out-of-order events are possible, embed versioning or
+// timestamps in the mutation and compare before applying changes.
+func init() {
+ RegisterMutation[messages.OrderCreated](
+ "OrderCreated",
+ func(g *CartGrain, m *messages.OrderCreated) error {
+ if m == nil {
+ return fmt.Errorf("OrderCreated: nil payload")
+ }
+ if m.OrderId == "" {
+ return fmt.Errorf("OrderCreated: missing orderId")
+ }
+
+ g.OrderReference = m.OrderId
+ g.PaymentStatus = m.Status
+ g.PaymentInProgress = false
+ return nil
+ },
+ // No WithTotals(): order completion does not modify pricing or taxes.
+ )
+}
diff --git a/mutation_registry.go b/mutation_registry.go
new file mode 100644
index 0000000..975ec22
--- /dev/null
+++ b/mutation_registry.go
@@ -0,0 +1,301 @@
+package main
+
+import (
+ "fmt"
+ "reflect"
+ "sync"
+)
+
+// mutation_registry.go
+//
+// Mutation Registry Infrastructure
+// --------------------------------
+// This file introduces a generic registry for cart mutations that:
+//
+// 1. Decouples mutation logic from the large type-switch inside CartGrain.Apply.
+// 2. Enforces (at registration time) that every mutation handler has the correct
+// signature: func(*CartGrain, *T) error
+// 3. Optionally auto-updates cart totals after a mutation if flagged.
+// 4. Provides a single authoritative list of registered mutations for
+// introspection / coverage testing.
+// 5. Allows incremental migration: you can first register new mutations here,
+// and later prune the legacy switch cases.
+//
+// Usage Pattern
+// -------------
+// // Define your mutation proto message (e.g. messages.ApplyCoupon in messages.proto)
+// // Regenerate protobufs.
+//
+// // In an init() (ideally in a small file like mutations_apply_coupon.go)
+// func init() {
+// RegisterMutation[*messages.ApplyCoupon](
+// "ApplyCoupon",
+// func(g *CartGrain, m *messages.ApplyCoupon) error {
+// // domain logic ...
+// discount := int64(5000)
+// if g.TotalPrice < discount {
+// discount = g.TotalPrice
+// }
+// g.TotalDiscount += discount
+// g.TotalPrice -= discount
+// return nil
+// },
+// WithTotals(), // we changed price-related fields; recalc totals
+// )
+// }
+//
+// // To invoke dynamically (alternative to the current switch):
+// if updated, err := ApplyRegistered(grain, incomingMessage); err == nil {
+// grain = updated
+// } else if errors.Is(err, ErrMutationNotRegistered) {
+// // fallback to legacy switch logic
+// }
+//
+// Migration Strategy
+// ------------------
+// 1. For each existing mutation handled in CartGrain.Apply, add a registry
+// registration with equivalent logic.
+// 2. Add a test that enumerates all *expected* mutation proto types and asserts
+// they are present in RegisteredMutationTypes().
+// 3. Once coverage is 100%, replace the switch in CartGrain.Apply with a call
+// to ApplyRegistered (and optionally keep a minimal default to produce an
+// "unsupported mutation" error).
+//
+// Thread Safety
+// -------------
+// Registration is typically done at init() time; a RWMutex provides safety
+// should late dynamic registration ever be introduced.
+//
+// Auto Totals
+// -----------
+// Many mutations require recomputing totals. To avoid forgetting this, pass
+// WithTotals() when registering. This will invoke grain.UpdateTotals() after
+// the handler returns successfully.
+//
+// Error Semantics
+// ---------------
+// - If a handler returns an error, totals are NOT recalculated (even if
+// WithTotals() was specified).
+// - ApplyRegistered returns (nil, ErrMutationNotRegistered) if the message type
+// is absent.
+//
+// Extensibility
+// -------------
+// It is straightforward to add options like audit hooks, metrics wrappers,
+// or optimistic concurrency guards by extending MutationOption.
+//
+// NOTE: Generics require Go 1.18+. If constrained to earlier Go versions,
+// replace the generic registration with a non-generic RegisterMutationType
+// that accepts reflect.Type and an adapter function.
+//
+// ---------------------------------------------------------------------------
+
+var (
+ mutationRegistryMu sync.RWMutex
+ mutationRegistry = make(map[reflect.Type]*registeredMutation)
+
+ // ErrMutationNotRegistered is returned when no handler exists for a given mutation type.
+ ErrMutationNotRegistered = fmt.Errorf("mutation not registered")
+)
+
+// MutationOption configures additional behavior for a registered mutation.
+type MutationOption func(*mutationOptions)
+
+// mutationOptions holds flags adjustable per registration.
+type mutationOptions struct {
+ updateTotals bool
+}
+
+// WithTotals ensures CartGrain.UpdateTotals() is called after a successful handler.
+func WithTotals() MutationOption {
+ return func(o *mutationOptions) {
+ o.updateTotals = true
+ }
+}
+
+// registeredMutation stores metadata + the execution closure.
+type registeredMutation struct {
+ name string
+ handler func(*CartGrain, interface{}) error
+ updateTotals bool
+ msgType reflect.Type
+}
+
+// RegisterMutation registers a mutation handler for a specific message type T.
+//
+// Parameters:
+//
+// name - a human-readable identifier (used for diagnostics / coverage tests).
+// handler - business logic operating on the cart grain & strongly typed message.
+// options - optional behavior flags (e.g., WithTotals()).
+//
+// Panics if:
+// - name is empty
+// - handler is nil
+// - duplicate registration for the same message type T
+//
+// Typical call is placed in an init() function.
+func RegisterMutation[T any](name string, handler func(*CartGrain, *T) error, options ...MutationOption) {
+ if name == "" {
+ panic("RegisterMutation: name is required")
+ }
+ if handler == nil {
+ panic("RegisterMutation: handler is nil")
+ }
+
+ // Derive the reflect.Type for *T then its Elem (T) for mapping.
+ var zero *T
+ rtPtr := reflect.TypeOf(zero)
+ if rtPtr.Kind() != reflect.Ptr {
+ panic("RegisterMutation: expected pointer type for generic parameter")
+ }
+ rt := rtPtr.Elem()
+
+ opts := mutationOptions{}
+ for _, opt := range options {
+ opt(&opts)
+ }
+
+ wrapped := func(g *CartGrain, m interface{}) error {
+ typed, ok := m.(*T)
+ if !ok {
+ return fmt.Errorf("mutation type mismatch: have %T want *%s", m, rt.Name())
+ }
+ return handler(g, typed)
+ }
+
+ mutationRegistryMu.Lock()
+ defer mutationRegistryMu.Unlock()
+
+ if _, exists := mutationRegistry[rt]; exists {
+ panic(fmt.Sprintf("RegisterMutation: duplicate registration for type %s", rt.String()))
+ }
+
+ mutationRegistry[rt] = ®isteredMutation{
+ name: name,
+ handler: wrapped,
+ updateTotals: opts.updateTotals,
+ msgType: rt,
+ }
+}
+
+// ApplyRegistered attempts to apply a registered mutation.
+// Returns updated grain if successful.
+//
+// If the mutation is not registered, returns (nil, ErrMutationNotRegistered).
+func ApplyRegistered(grain *CartGrain, msg interface{}) (*CartGrain, error) {
+ if grain == nil {
+ return nil, fmt.Errorf("nil grain")
+ }
+ if msg == nil {
+ return nil, fmt.Errorf("nil mutation message")
+ }
+
+ rt := indirectType(reflect.TypeOf(msg))
+ mutationRegistryMu.RLock()
+ entry, ok := mutationRegistry[rt]
+ mutationRegistryMu.RUnlock()
+
+ if !ok {
+ return nil, ErrMutationNotRegistered
+ }
+
+ if err := entry.handler(grain, msg); err != nil {
+ return nil, err
+ }
+
+ if entry.updateTotals {
+ grain.UpdateTotals()
+ }
+
+ return grain, nil
+}
+
+// RegisteredMutations returns metadata for all registered mutations (snapshot).
+func RegisteredMutations() []string {
+ mutationRegistryMu.RLock()
+ defer mutationRegistryMu.RUnlock()
+ out := make([]string, 0, len(mutationRegistry))
+ for _, entry := range mutationRegistry {
+ out = append(out, entry.name)
+ }
+ return out
+}
+
+// RegisteredMutationTypes returns the reflect.Type list of all registered messages.
+// Useful for coverage tests ensuring expected set matches actual set.
+func RegisteredMutationTypes() []reflect.Type {
+ mutationRegistryMu.RLock()
+ defer mutationRegistryMu.RUnlock()
+ out := make([]reflect.Type, 0, len(mutationRegistry))
+ for t := range mutationRegistry {
+ out = append(out, t)
+ }
+ return out
+}
+
+// MustAssertMutationCoverage can be called at startup to ensure every expected
+// mutation type has been registered. It panics with a descriptive message if any
+// are missing. Provide a slice of prototype pointers (e.g. []*messages.AddItem{nil} ...)
+func MustAssertMutationCoverage(expected []interface{}) {
+ mutationRegistryMu.RLock()
+ defer mutationRegistryMu.RUnlock()
+
+ missing := make([]string, 0)
+ for _, ex := range expected {
+ if ex == nil {
+ continue
+ }
+ t := indirectType(reflect.TypeOf(ex))
+ if _, ok := mutationRegistry[t]; !ok {
+ missing = append(missing, t.String())
+ }
+ }
+ if len(missing) > 0 {
+ panic(fmt.Sprintf("mutation registry missing handlers for: %v", missing))
+ }
+}
+
+// indirectType returns the element type if given a pointer; otherwise the type itself.
+func indirectType(t reflect.Type) reflect.Type {
+ for t.Kind() == reflect.Ptr {
+ t = t.Elem()
+ }
+ return t
+}
+
+/*
+Integration Guide
+-----------------
+
+1. Register all existing mutations:
+
+ func init() {
+ RegisterMutation[*messages.AddItem]("AddItem",
+ func(g *CartGrain, m *messages.AddItem) error {
+ // (port logic from existing switch branch)
+ // ...
+ return nil
+ },
+ WithTotals(),
+ )
+ // ... repeat for others
+ }
+
+2. In CartGrain.Apply (early in the method) add:
+
+ if updated, err := ApplyRegistered(c, content); err == nil {
+ return updated, nil
+ } else if err != ErrMutationNotRegistered {
+ return nil, err
+ }
+
+ // existing switch fallback below
+
+3. Once all mutations are registered, remove the legacy switch cases
+ and leave a single ErrMutationNotRegistered path for unknown types.
+
+4. Add a coverage test (see docs for example; removed from source for clarity).
+5. (Optional) Add metrics / tracing wrappers for handlers.
+
+*/
diff --git a/mutation_remove_delivery.go b/mutation_remove_delivery.go
new file mode 100644
index 0000000..49e9dcf
--- /dev/null
+++ b/mutation_remove_delivery.go
@@ -0,0 +1,53 @@
+package main
+
+import (
+ "fmt"
+
+ messages "git.tornberg.me/go-cart-actor/proto"
+)
+
+// mutation_remove_delivery.go
+//
+// Registers the RemoveDelivery mutation.
+//
+// Behavior:
+// - Removes the delivery entry whose Id == payload.Id.
+// - If not found, returns an error.
+// - Cart totals are recalculated (WithTotals) after removal.
+// - Items previously associated with that delivery simply become "without delivery";
+// subsequent delivery mutations can reassign them.
+//
+// Differences vs legacy:
+// - Legacy logic decremented TotalPrice explicitly before recalculating.
+// Here we rely solely on UpdateTotals() to recompute from remaining
+// deliveries and items (simpler / single source of truth).
+//
+// Future considerations:
+// - If delivery pricing logic changes (e.g., dynamic taxes per delivery),
+// UpdateTotals() may need enhancement to incorporate delivery tax properly.
+func init() {
+ RegisterMutation[messages.RemoveDelivery](
+ "RemoveDelivery",
+ func(g *CartGrain, m *messages.RemoveDelivery) error {
+ if m == nil {
+ return fmt.Errorf("RemoveDelivery: nil payload")
+ }
+ targetID := int(m.Id)
+ index := -1
+ for i, d := range g.Deliveries {
+ if d.Id == targetID {
+ index = i
+ break
+ }
+ }
+ if index == -1 {
+ return fmt.Errorf("RemoveDelivery: delivery id %d not found", m.Id)
+ }
+
+ // Remove delivery (order not preserved beyond necessity)
+ g.Deliveries = append(g.Deliveries[:index], g.Deliveries[index+1:]...)
+ return nil
+ },
+ WithTotals(),
+ )
+}
diff --git a/mutation_remove_item.go b/mutation_remove_item.go
new file mode 100644
index 0000000..420eff0
--- /dev/null
+++ b/mutation_remove_item.go
@@ -0,0 +1,49 @@
+package main
+
+import (
+ "fmt"
+
+ messages "git.tornberg.me/go-cart-actor/proto"
+)
+
+// mutation_remove_item.go
+//
+// Registers the RemoveItem mutation.
+//
+// Behavior:
+// - Removes the cart line whose local cart line Id == payload.Id
+// - If no such line exists returns an error
+// - Recalculates cart totals (WithTotals)
+//
+// Notes:
+// - This removes only the line item; any deliveries referencing the removed
+// item are NOT automatically adjusted (mirrors prior logic). If future
+// semantics require pruning delivery.item_ids you can extend this handler.
+// - If multiple lines somehow shared the same Id (should not happen), only
+// the first match would be removed—data integrity relies on unique line Ids.
+func init() {
+ RegisterMutation[messages.RemoveItem](
+ "RemoveItem",
+ func(g *CartGrain, m *messages.RemoveItem) error {
+ if m == nil {
+ return fmt.Errorf("RemoveItem: nil payload")
+ }
+ targetID := int(m.Id)
+
+ index := -1
+ for i, it := range g.Items {
+ if it.Id == targetID {
+ index = i
+ break
+ }
+ }
+ if index == -1 {
+ return fmt.Errorf("RemoveItem: item id %d not found", m.Id)
+ }
+
+ g.Items = append(g.Items[:index], g.Items[index+1:]...)
+ return nil
+ },
+ WithTotals(),
+ )
+}
diff --git a/mutation_set_cart_items.go b/mutation_set_cart_items.go
new file mode 100644
index 0000000..aa811a7
--- /dev/null
+++ b/mutation_set_cart_items.go
@@ -0,0 +1,57 @@
+package main
+
+import (
+ "fmt"
+
+ messages "git.tornberg.me/go-cart-actor/proto"
+)
+
+// mutation_set_cart_items.go
+//
+// Registers the SetCartRequest mutation. This mutation replaces the entire list
+// of cart items with the provided list (each entry is an AddRequest).
+//
+// Behavior:
+// - Clears existing items (but leaves deliveries intact).
+// - Iterates over each AddRequest and delegates to CartGrain.AddItem
+// (which performs product lookup, creates AddItem mutation).
+// - If any single addition fails, the mutation aborts with an error;
+// items added prior to the failure remain (consistent with previous behavior).
+// - Totals recalculated after completion via WithTotals().
+//
+// Notes:
+// - Potential optimization: batch product lookups; currently sequential.
+// - Consider adding rollback semantics if atomic replacement is desired.
+// - Deliveries might reference item IDs that are now invalid—original logic
+// also left deliveries untouched. If that becomes an issue, add a cleanup
+// pass to remove deliveries whose item IDs no longer exist.
+func init() {
+ RegisterMutation[messages.SetCartRequest](
+ "SetCartRequest",
+ func(g *CartGrain, m *messages.SetCartRequest) error {
+ if m == nil {
+ return fmt.Errorf("SetCartRequest: nil payload")
+ }
+
+ // Clear current items (keep deliveries)
+ g.mu.Lock()
+ g.Items = make([]*CartItem, 0, len(m.Items))
+ g.mu.Unlock()
+
+ for _, it := range m.Items {
+ if it == nil {
+ continue
+ }
+ if it.Sku == "" || it.Quantity < 1 {
+ return fmt.Errorf("SetCartRequest: invalid item (sku='%s' qty=%d)", it.Sku, it.Quantity)
+ }
+ _, err := g.AddItem(it.Sku, int(it.Quantity), it.Country, it.StoreId)
+ if err != nil {
+ return fmt.Errorf("SetCartRequest: add sku '%s' failed: %w", it.Sku, err)
+ }
+ }
+ return nil
+ },
+ WithTotals(),
+ )
+}
diff --git a/mutation_set_delivery.go b/mutation_set_delivery.go
new file mode 100644
index 0000000..3cea3c7
--- /dev/null
+++ b/mutation_set_delivery.go
@@ -0,0 +1,101 @@
+package main
+
+import (
+ "fmt"
+ "slices"
+
+ messages "git.tornberg.me/go-cart-actor/proto"
+)
+
+// mutation_set_delivery.go
+//
+// Registers the SetDelivery mutation.
+//
+// Semantics (mirrors legacy switch logic):
+// - If the payload specifies an explicit list of item IDs (payload.Items):
+// - Each referenced cart line must exist.
+// - None of the referenced items may already belong to a delivery.
+// - Only those items are associated with the new delivery.
+// - If payload.Items is empty:
+// - All items currently without any delivery are associated with the new delivery.
+// - A new delivery line is created with:
+// - Auto-incremented delivery ID (cart-local)
+// - Provider from payload
+// - Fixed price (currently hard-coded: 4900 minor units) – adjust as needed
+// - Optional PickupPoint copied from payload
+// - Cart totals are recalculated (WithTotals)
+//
+// Error cases:
+// - Referenced item does not exist
+// - Referenced item already has a delivery
+// - No items qualify (resulting association set empty) -> returns error (prevents creating empty delivery)
+//
+// Concurrency:
+// - Uses g.mu to protect lastDeliveryId increment and append to Deliveries slice.
+// Item scans are read-only and performed outside the lock for simplicity;
+// if stricter guarantees are needed, widen the lock section.
+//
+// Future extension points:
+// - Variable delivery pricing (based on weight, distance, provider, etc.)
+// - Validation of provider codes
+// - Multi-currency delivery pricing
+func init() {
+ RegisterMutation[messages.SetDelivery](
+ "SetDelivery",
+ func(g *CartGrain, m *messages.SetDelivery) error {
+ if m == nil {
+ return fmt.Errorf("SetDelivery: nil payload")
+ }
+ if m.Provider == "" {
+ return fmt.Errorf("SetDelivery: provider is empty")
+ }
+
+ withDelivery := g.ItemsWithDelivery()
+ targetItems := make([]int, 0)
+
+ if len(m.Items) == 0 {
+ // Use every item currently without a delivery
+ targetItems = append(targetItems, g.ItemsWithoutDelivery()...)
+ } else {
+ // Validate explicit list
+ for _, id64 := range m.Items {
+ id := int(id64)
+ found := false
+ for _, it := range g.Items {
+ if it.Id == id {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return fmt.Errorf("SetDelivery: item id %d not found", id)
+ }
+ if slices.Contains(withDelivery, id) {
+ return fmt.Errorf("SetDelivery: item id %d already has a delivery", id)
+ }
+ targetItems = append(targetItems, id)
+ }
+ }
+
+ if len(targetItems) == 0 {
+ return fmt.Errorf("SetDelivery: no eligible items to attach")
+ }
+
+ // Append new delivery
+ g.mu.Lock()
+ g.lastDeliveryId++
+ newId := g.lastDeliveryId
+ g.Deliveries = append(g.Deliveries, &CartDelivery{
+ Id: newId,
+ Provider: m.Provider,
+ PickupPoint: m.PickupPoint,
+ Price: 4900, // TODO: externalize pricing
+ Items: targetItems,
+ })
+ g.mu.Unlock()
+
+ return nil
+ },
+ WithTotals(),
+ )
+}
diff --git a/mutation_set_pickup_point.go b/mutation_set_pickup_point.go
new file mode 100644
index 0000000..ba8b8fe
--- /dev/null
+++ b/mutation_set_pickup_point.go
@@ -0,0 +1,56 @@
+package main
+
+import (
+ "fmt"
+
+ messages "git.tornberg.me/go-cart-actor/proto"
+)
+
+// mutation_set_pickup_point.go
+//
+// Registers the SetPickupPoint mutation using the generic mutation registry.
+//
+// Semantics (mirrors original switch-based implementation):
+// - Locate the delivery with Id == payload.DeliveryId
+// - Set (or overwrite) its PickupPoint with the provided data
+// - Does NOT alter pricing or taxes (so no totals recalculation required)
+//
+// Validation / Error Handling:
+// - If payload is nil -> error
+// - If DeliveryId not found -> error
+//
+// Concurrency:
+// - Relies on the existing expectation that higher-level mutation routing
+// serializes Apply() calls per grain; if stricter guarantees are needed,
+// a delivery-level lock could be introduced later.
+//
+// Future Extensions:
+// - Validate pickup point fields (country code, zip format, etc.)
+// - Track history / audit of pickup point changes
+// - Trigger delivery price adjustments (which would then require WithTotals()).
+func init() {
+ RegisterMutation[messages.SetPickupPoint](
+ "SetPickupPoint",
+ func(g *CartGrain, m *messages.SetPickupPoint) error {
+ if m == nil {
+ return fmt.Errorf("SetPickupPoint: nil payload")
+ }
+
+ for _, d := range g.Deliveries {
+ if d.Id == int(m.DeliveryId) {
+ d.PickupPoint = &messages.PickupPoint{
+ Id: m.Id,
+ Name: m.Name,
+ Address: m.Address,
+ City: m.City,
+ Zip: m.Zip,
+ Country: m.Country,
+ }
+ return nil
+ }
+ }
+ return fmt.Errorf("SetPickupPoint: delivery id %d not found", m.DeliveryId)
+ },
+ // No WithTotals(): pickup point does not change pricing / tax.
+ )
+}
diff --git a/pool-server.go b/pool-server.go
new file mode 100644
index 0000000..bc72f19
--- /dev/null
+++ b/pool-server.go
@@ -0,0 +1,351 @@
+package main
+
+import (
+ "bytes"
+ "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) process(id CartId, mutation interface{}) (*messages.CartState, error) {
+ grain, err := s.pool.Apply(id, mutation)
+ if err != nil {
+ return nil, err
+ }
+ return ToCartState(grain), nil
+}
+
+func (s *PoolServer) HandleGet(w http.ResponseWriter, r *http.Request, id CartId) error {
+ grain, err := s.pool.Get(id)
+ if err != nil {
+ return err
+ }
+
+ return s.WriteResult(w, ToCartState(grain))
+}
+
+func (s *PoolServer) HandleAddSku(w http.ResponseWriter, r *http.Request, id CartId) error {
+ sku := r.PathValue("sku")
+ data, err := s.process(id, &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 *messages.CartState) error {
+ w.Header().Set("Content-Type", "application/json")
+ w.Header().Set("Cache-Control", "no-cache")
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("X-Pod-Name", s.pod_name)
+ if result == nil {
+
+ w.WriteHeader(http.StatusInternalServerError)
+
+ return nil
+ }
+ w.WriteHeader(http.StatusOK)
+ enc := json.NewEncoder(w)
+ err := enc.Encode(result)
+ return err
+}
+
+func (s *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.process(id, &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.process(id, &messages.SetDelivery{
+ Provider: delivery.Provider,
+ Items: delivery.Items,
+ PickupPoint: delivery.PickupPoint,
+ })
+ if err != nil {
+ return err
+ }
+ return s.WriteResult(w, data)
+}
+
+func (s *PoolServer) 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.process(id, &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.process(id, &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.process(id, &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.process(id, &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.process(id, &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 {
+ // Build checkout meta (URLs derived from host)
+ meta := &CheckoutMeta{
+ Terms: fmt.Sprintf("https://%s/terms", r.Host),
+ Checkout: fmt.Sprintf("https://%s/checkout?order_id={checkout.order.id}", r.Host),
+ Confirmation: fmt.Sprintf("https://%s/confirmation/{checkout.order.id}", r.Host),
+ Validation: fmt.Sprintf("https://%s/validate", r.Host),
+ Push: fmt.Sprintf("https://%s/push?order_id={checkout.order.id}", r.Host),
+ Country: getCountryFromHost(r.Host),
+ }
+
+ // Get current grain state (may be local or remote)
+ grain, err := s.pool.Get(id)
+ if err != nil {
+ return err
+ }
+
+ // Build pure checkout payload
+ payload, _, err := BuildCheckoutOrderPayload(grain, meta)
+ if err != nil {
+ return err
+ }
+
+ // Call Klarna (create or update)
+ var klarnaOrder *CheckoutOrder
+ if grain.OrderReference != "" {
+ klarnaOrder, err = KlarnaInstance.UpdateOrder(grain.OrderReference, bytes.NewReader(payload))
+ } else {
+ klarnaOrder, err = KlarnaInstance.CreateOrder(bytes.NewReader(payload))
+ }
+ if err != nil {
+ return err
+ }
+
+ // Persist initialization state via mutation (best-effort)
+ if _, applyErr := s.pool.Apply(id, &messages.InitializeCheckout{
+ OrderId: klarnaOrder.ID,
+ Status: klarnaOrder.Status,
+ PaymentInProgress: true,
+ }); applyErr != nil {
+ log.Printf("InitializeCheckout apply error: %v", applyErr)
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ return json.NewEncoder(w).Encode(klarnaOrder)
+}
+
+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: r.TLS != nil,
+ 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: r.TLS != nil,
+ 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 f7f1a73..c3c5249 100644
--- a/proto/cart_actor.pb.go
+++ b/proto/cart_actor.pb.go
@@ -21,45 +21,34 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
-// 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"`
- 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:
+// Shared reply for all mutation RPCs.
+type CartMutationReply struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ StatusCode int32 `protobuf:"varint,1,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` // HTTP-like status (200 success, 4xx client, 5xx server)
+ // Types that are valid to be assigned to Result:
//
- // *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
+ // *CartMutationReply_State
+ // *CartMutationReply_Error
+ Result isCartMutationReply_Result `protobuf_oneof:"result"`
+ ServerTimestamp int64 `protobuf:"varint,4,opt,name=server_timestamp,json=serverTimestamp,proto3" json:"server_timestamp,omitempty"` // Server-assigned Unix timestamp (optional auditing)
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
}
-func (x *MutationEnvelope) Reset() {
- *x = MutationEnvelope{}
+func (x *CartMutationReply) Reset() {
+ *x = CartMutationReply{}
mi := &file_cart_actor_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
-func (x *MutationEnvelope) String() string {
+func (x *CartMutationReply) String() string {
return protoimpl.X.MessageStringOf(x)
}
-func (*MutationEnvelope) ProtoMessage() {}
+func (*CartMutationReply) ProtoMessage() {}
-func (x *MutationEnvelope) ProtoReflect() protoreflect.Message {
+func (x *CartMutationReply) ProtoReflect() protoreflect.Message {
mi := &file_cart_actor_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -71,281 +60,67 @@ func (x *MutationEnvelope) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
-// Deprecated: Use MutationEnvelope.ProtoReflect.Descriptor instead.
-func (*MutationEnvelope) Descriptor() ([]byte, []int) {
+// Deprecated: Use CartMutationReply.ProtoReflect.Descriptor instead.
+func (*CartMutationReply) Descriptor() ([]byte, []int) {
return file_cart_actor_proto_rawDescGZIP(), []int{0}
}
-func (x *MutationEnvelope) GetCartId() string {
- if x != nil {
- return x.CartId
- }
- return ""
-}
-
-func (x *MutationEnvelope) GetClientTimestamp() int64 {
- if x != nil {
- return x.ClientTimestamp
- }
- return 0
-}
-
-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"`
- // 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
-}
-
-func (x *MutationReply) Reset() {
- *x = MutationReply{}
- mi := &file_cart_actor_proto_msgTypes[1]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *MutationReply) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*MutationReply) ProtoMessage() {}
-
-func (x *MutationReply) ProtoReflect() protoreflect.Message {
- mi := &file_cart_actor_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 MutationReply.ProtoReflect.Descriptor instead.
-func (*MutationReply) Descriptor() ([]byte, []int) {
- return file_cart_actor_proto_rawDescGZIP(), []int{1}
-}
-
-func (x *MutationReply) GetStatusCode() int32 {
+func (x *CartMutationReply) GetStatusCode() int32 {
if x != nil {
return x.StatusCode
}
return 0
}
-func (x *MutationReply) GetResult() isMutationReply_Result {
+func (x *CartMutationReply) GetResult() isCartMutationReply_Result {
if x != nil {
return x.Result
}
return nil
}
-func (x *MutationReply) GetState() *CartState {
+func (x *CartMutationReply) GetState() *CartState {
if x != nil {
- if x, ok := x.Result.(*MutationReply_State); ok {
+ if x, ok := x.Result.(*CartMutationReply_State); ok {
return x.State
}
}
return nil
}
-func (x *MutationReply) GetError() string {
+func (x *CartMutationReply) GetError() string {
if x != nil {
- if x, ok := x.Result.(*MutationReply_Error); ok {
+ if x, ok := x.Result.(*CartMutationReply_Error); ok {
return x.Error
}
}
return ""
}
-type isMutationReply_Result interface {
- isMutationReply_Result()
+func (x *CartMutationReply) GetServerTimestamp() int64 {
+ if x != nil {
+ return x.ServerTimestamp
+ }
+ return 0
}
-type MutationReply_State struct {
- State *CartState `protobuf:"bytes,2,opt,name=state,proto3,oneof"`
+type isCartMutationReply_Result interface {
+ isCartMutationReply_Result()
}
-type MutationReply_Error struct {
- Error string `protobuf:"bytes,3,opt,name=error,proto3,oneof"`
+type CartMutationReply_State struct {
+ State *CartState `protobuf:"bytes,2,opt,name=state,proto3,oneof"` // Updated cart state on success
}
-func (*MutationReply_State) isMutationReply_Result() {}
+type CartMutationReply_Error struct {
+ Error string `protobuf:"bytes,3,opt,name=error,proto3,oneof"` // Error message on failure
+}
-func (*MutationReply_Error) isMutationReply_Result() {}
+func (*CartMutationReply_State) isCartMutationReply_Result() {}
-// StateRequest fetches current cart state without mutating.
+func (*CartMutationReply_Error) isCartMutationReply_Result() {}
+
+// Fetch current cart state without mutation.
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"`
@@ -355,7 +130,7 @@ type StateRequest struct {
func (x *StateRequest) Reset() {
*x = StateRequest{}
- mi := &file_cart_actor_proto_msgTypes[2]
+ mi := &file_cart_actor_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -367,7 +142,7 @@ func (x *StateRequest) String() string {
func (*StateRequest) ProtoMessage() {}
func (x *StateRequest) ProtoReflect() protoreflect.Message {
- mi := &file_cart_actor_proto_msgTypes[2]
+ mi := &file_cart_actor_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -380,7 +155,7 @@ func (x *StateRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use StateRequest.ProtoReflect.Descriptor instead.
func (*StateRequest) Descriptor() ([]byte, []int) {
- return file_cart_actor_proto_rawDescGZIP(), []int{2}
+ return file_cart_actor_proto_rawDescGZIP(), []int{1}
}
func (x *StateRequest) GetCartId() string {
@@ -390,9 +165,698 @@ func (x *StateRequest) GetCartId() string {
return ""
}
+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"`
+ // 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[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *StateReply) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StateReply) ProtoMessage() {}
+
+func (x *StateReply) ProtoReflect() protoreflect.Message {
+ mi := &file_cart_actor_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 StateReply.ProtoReflect.Descriptor instead.
+func (*StateReply) Descriptor() ([]byte, []int) {
+ return file_cart_actor_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *StateReply) GetStatusCode() int32 {
+ if x != nil {
+ return x.StatusCode
+ }
+ return 0
+}
+
+func (x *StateReply) GetResult() isStateReply_Result {
+ if x != nil {
+ 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() {}
+
+type AddRequestRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ CartId string `protobuf:"bytes,1,opt,name=cart_id,json=cartId,proto3" json:"cart_id,omitempty"`
+ ClientTimestamp int64 `protobuf:"varint,2,opt,name=client_timestamp,json=clientTimestamp,proto3" json:"client_timestamp,omitempty"`
+ Payload *AddRequest `protobuf:"bytes,10,opt,name=payload,proto3" json:"payload,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *AddRequestRequest) Reset() {
+ *x = AddRequestRequest{}
+ mi := &file_cart_actor_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AddRequestRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AddRequestRequest) ProtoMessage() {}
+
+func (x *AddRequestRequest) 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 AddRequestRequest.ProtoReflect.Descriptor instead.
+func (*AddRequestRequest) Descriptor() ([]byte, []int) {
+ return file_cart_actor_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *AddRequestRequest) GetCartId() string {
+ if x != nil {
+ return x.CartId
+ }
+ return ""
+}
+
+func (x *AddRequestRequest) GetClientTimestamp() int64 {
+ if x != nil {
+ return x.ClientTimestamp
+ }
+ return 0
+}
+
+func (x *AddRequestRequest) GetPayload() *AddRequest {
+ if x != nil {
+ return x.Payload
+ }
+ return nil
+}
+
+type AddItemRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ CartId string `protobuf:"bytes,1,opt,name=cart_id,json=cartId,proto3" json:"cart_id,omitempty"`
+ ClientTimestamp int64 `protobuf:"varint,2,opt,name=client_timestamp,json=clientTimestamp,proto3" json:"client_timestamp,omitempty"`
+ Payload *AddItem `protobuf:"bytes,10,opt,name=payload,proto3" json:"payload,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *AddItemRequest) Reset() {
+ *x = AddItemRequest{}
+ mi := &file_cart_actor_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AddItemRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AddItemRequest) ProtoMessage() {}
+
+func (x *AddItemRequest) 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 AddItemRequest.ProtoReflect.Descriptor instead.
+func (*AddItemRequest) Descriptor() ([]byte, []int) {
+ return file_cart_actor_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *AddItemRequest) GetCartId() string {
+ if x != nil {
+ return x.CartId
+ }
+ return ""
+}
+
+func (x *AddItemRequest) GetClientTimestamp() int64 {
+ if x != nil {
+ return x.ClientTimestamp
+ }
+ return 0
+}
+
+func (x *AddItemRequest) GetPayload() *AddItem {
+ if x != nil {
+ return x.Payload
+ }
+ return nil
+}
+
+type RemoveItemRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ CartId string `protobuf:"bytes,1,opt,name=cart_id,json=cartId,proto3" json:"cart_id,omitempty"`
+ ClientTimestamp int64 `protobuf:"varint,2,opt,name=client_timestamp,json=clientTimestamp,proto3" json:"client_timestamp,omitempty"`
+ Payload *RemoveItem `protobuf:"bytes,10,opt,name=payload,proto3" json:"payload,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *RemoveItemRequest) Reset() {
+ *x = RemoveItemRequest{}
+ mi := &file_cart_actor_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *RemoveItemRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RemoveItemRequest) ProtoMessage() {}
+
+func (x *RemoveItemRequest) 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 RemoveItemRequest.ProtoReflect.Descriptor instead.
+func (*RemoveItemRequest) Descriptor() ([]byte, []int) {
+ return file_cart_actor_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *RemoveItemRequest) GetCartId() string {
+ if x != nil {
+ return x.CartId
+ }
+ return ""
+}
+
+func (x *RemoveItemRequest) GetClientTimestamp() int64 {
+ if x != nil {
+ return x.ClientTimestamp
+ }
+ return 0
+}
+
+func (x *RemoveItemRequest) GetPayload() *RemoveItem {
+ if x != nil {
+ return x.Payload
+ }
+ return nil
+}
+
+type RemoveDeliveryRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ CartId string `protobuf:"bytes,1,opt,name=cart_id,json=cartId,proto3" json:"cart_id,omitempty"`
+ ClientTimestamp int64 `protobuf:"varint,2,opt,name=client_timestamp,json=clientTimestamp,proto3" json:"client_timestamp,omitempty"`
+ Payload *RemoveDelivery `protobuf:"bytes,10,opt,name=payload,proto3" json:"payload,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *RemoveDeliveryRequest) Reset() {
+ *x = RemoveDeliveryRequest{}
+ mi := &file_cart_actor_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *RemoveDeliveryRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RemoveDeliveryRequest) ProtoMessage() {}
+
+func (x *RemoveDeliveryRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_cart_actor_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 RemoveDeliveryRequest.ProtoReflect.Descriptor instead.
+func (*RemoveDeliveryRequest) Descriptor() ([]byte, []int) {
+ return file_cart_actor_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *RemoveDeliveryRequest) GetCartId() string {
+ if x != nil {
+ return x.CartId
+ }
+ return ""
+}
+
+func (x *RemoveDeliveryRequest) GetClientTimestamp() int64 {
+ if x != nil {
+ return x.ClientTimestamp
+ }
+ return 0
+}
+
+func (x *RemoveDeliveryRequest) GetPayload() *RemoveDelivery {
+ if x != nil {
+ return x.Payload
+ }
+ return nil
+}
+
+type ChangeQuantityRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ CartId string `protobuf:"bytes,1,opt,name=cart_id,json=cartId,proto3" json:"cart_id,omitempty"`
+ ClientTimestamp int64 `protobuf:"varint,2,opt,name=client_timestamp,json=clientTimestamp,proto3" json:"client_timestamp,omitempty"`
+ Payload *ChangeQuantity `protobuf:"bytes,10,opt,name=payload,proto3" json:"payload,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ChangeQuantityRequest) Reset() {
+ *x = ChangeQuantityRequest{}
+ mi := &file_cart_actor_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ChangeQuantityRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ChangeQuantityRequest) ProtoMessage() {}
+
+func (x *ChangeQuantityRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_cart_actor_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 ChangeQuantityRequest.ProtoReflect.Descriptor instead.
+func (*ChangeQuantityRequest) Descriptor() ([]byte, []int) {
+ return file_cart_actor_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *ChangeQuantityRequest) GetCartId() string {
+ if x != nil {
+ return x.CartId
+ }
+ return ""
+}
+
+func (x *ChangeQuantityRequest) GetClientTimestamp() int64 {
+ if x != nil {
+ return x.ClientTimestamp
+ }
+ return 0
+}
+
+func (x *ChangeQuantityRequest) GetPayload() *ChangeQuantity {
+ if x != nil {
+ return x.Payload
+ }
+ return nil
+}
+
+type SetDeliveryRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ CartId string `protobuf:"bytes,1,opt,name=cart_id,json=cartId,proto3" json:"cart_id,omitempty"`
+ ClientTimestamp int64 `protobuf:"varint,2,opt,name=client_timestamp,json=clientTimestamp,proto3" json:"client_timestamp,omitempty"`
+ Payload *SetDelivery `protobuf:"bytes,10,opt,name=payload,proto3" json:"payload,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SetDeliveryRequest) Reset() {
+ *x = SetDeliveryRequest{}
+ mi := &file_cart_actor_proto_msgTypes[8]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SetDeliveryRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SetDeliveryRequest) ProtoMessage() {}
+
+func (x *SetDeliveryRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_cart_actor_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 SetDeliveryRequest.ProtoReflect.Descriptor instead.
+func (*SetDeliveryRequest) Descriptor() ([]byte, []int) {
+ return file_cart_actor_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *SetDeliveryRequest) GetCartId() string {
+ if x != nil {
+ return x.CartId
+ }
+ return ""
+}
+
+func (x *SetDeliveryRequest) GetClientTimestamp() int64 {
+ if x != nil {
+ return x.ClientTimestamp
+ }
+ return 0
+}
+
+func (x *SetDeliveryRequest) GetPayload() *SetDelivery {
+ if x != nil {
+ return x.Payload
+ }
+ return nil
+}
+
+type SetPickupPointRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ CartId string `protobuf:"bytes,1,opt,name=cart_id,json=cartId,proto3" json:"cart_id,omitempty"`
+ ClientTimestamp int64 `protobuf:"varint,2,opt,name=client_timestamp,json=clientTimestamp,proto3" json:"client_timestamp,omitempty"`
+ Payload *SetPickupPoint `protobuf:"bytes,10,opt,name=payload,proto3" json:"payload,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SetPickupPointRequest) Reset() {
+ *x = SetPickupPointRequest{}
+ mi := &file_cart_actor_proto_msgTypes[9]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SetPickupPointRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SetPickupPointRequest) ProtoMessage() {}
+
+func (x *SetPickupPointRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_cart_actor_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 SetPickupPointRequest.ProtoReflect.Descriptor instead.
+func (*SetPickupPointRequest) Descriptor() ([]byte, []int) {
+ return file_cart_actor_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *SetPickupPointRequest) GetCartId() string {
+ if x != nil {
+ return x.CartId
+ }
+ return ""
+}
+
+func (x *SetPickupPointRequest) GetClientTimestamp() int64 {
+ if x != nil {
+ return x.ClientTimestamp
+ }
+ return 0
+}
+
+func (x *SetPickupPointRequest) GetPayload() *SetPickupPoint {
+ if x != nil {
+ return x.Payload
+ }
+ return nil
+}
+
+type CreateCheckoutOrderRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ CartId string `protobuf:"bytes,1,opt,name=cart_id,json=cartId,proto3" json:"cart_id,omitempty"`
+ ClientTimestamp int64 `protobuf:"varint,2,opt,name=client_timestamp,json=clientTimestamp,proto3" json:"client_timestamp,omitempty"`
+ Payload *CreateCheckoutOrder `protobuf:"bytes,10,opt,name=payload,proto3" json:"payload,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CreateCheckoutOrderRequest) Reset() {
+ *x = CreateCheckoutOrderRequest{}
+ mi := &file_cart_actor_proto_msgTypes[10]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CreateCheckoutOrderRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CreateCheckoutOrderRequest) ProtoMessage() {}
+
+func (x *CreateCheckoutOrderRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_cart_actor_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 CreateCheckoutOrderRequest.ProtoReflect.Descriptor instead.
+func (*CreateCheckoutOrderRequest) Descriptor() ([]byte, []int) {
+ return file_cart_actor_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *CreateCheckoutOrderRequest) GetCartId() string {
+ if x != nil {
+ return x.CartId
+ }
+ return ""
+}
+
+func (x *CreateCheckoutOrderRequest) GetClientTimestamp() int64 {
+ if x != nil {
+ return x.ClientTimestamp
+ }
+ return 0
+}
+
+func (x *CreateCheckoutOrderRequest) GetPayload() *CreateCheckoutOrder {
+ if x != nil {
+ return x.Payload
+ }
+ return nil
+}
+
+type SetCartItemsRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ CartId string `protobuf:"bytes,1,opt,name=cart_id,json=cartId,proto3" json:"cart_id,omitempty"`
+ ClientTimestamp int64 `protobuf:"varint,2,opt,name=client_timestamp,json=clientTimestamp,proto3" json:"client_timestamp,omitempty"`
+ Payload *SetCartRequest `protobuf:"bytes,10,opt,name=payload,proto3" json:"payload,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SetCartItemsRequest) Reset() {
+ *x = SetCartItemsRequest{}
+ mi := &file_cart_actor_proto_msgTypes[11]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SetCartItemsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SetCartItemsRequest) ProtoMessage() {}
+
+func (x *SetCartItemsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_cart_actor_proto_msgTypes[11]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SetCartItemsRequest.ProtoReflect.Descriptor instead.
+func (*SetCartItemsRequest) Descriptor() ([]byte, []int) {
+ return file_cart_actor_proto_rawDescGZIP(), []int{11}
+}
+
+func (x *SetCartItemsRequest) GetCartId() string {
+ if x != nil {
+ return x.CartId
+ }
+ return ""
+}
+
+func (x *SetCartItemsRequest) GetClientTimestamp() int64 {
+ if x != nil {
+ return x.ClientTimestamp
+ }
+ return 0
+}
+
+func (x *SetCartItemsRequest) GetPayload() *SetCartRequest {
+ if x != nil {
+ return x.Payload
+ }
+ return nil
+}
+
+type OrderCompletedRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ CartId string `protobuf:"bytes,1,opt,name=cart_id,json=cartId,proto3" json:"cart_id,omitempty"`
+ ClientTimestamp int64 `protobuf:"varint,2,opt,name=client_timestamp,json=clientTimestamp,proto3" json:"client_timestamp,omitempty"`
+ Payload *OrderCreated `protobuf:"bytes,10,opt,name=payload,proto3" json:"payload,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *OrderCompletedRequest) Reset() {
+ *x = OrderCompletedRequest{}
+ mi := &file_cart_actor_proto_msgTypes[12]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *OrderCompletedRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*OrderCompletedRequest) ProtoMessage() {}
+
+func (x *OrderCompletedRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_cart_actor_proto_msgTypes[12]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use OrderCompletedRequest.ProtoReflect.Descriptor instead.
+func (*OrderCompletedRequest) Descriptor() ([]byte, []int) {
+ return file_cart_actor_proto_rawDescGZIP(), []int{12}
+}
+
+func (x *OrderCompletedRequest) GetCartId() string {
+ if x != nil {
+ return x.CartId
+ }
+ return ""
+}
+
+func (x *OrderCompletedRequest) GetClientTimestamp() int64 {
+ if x != nil {
+ return x.ClientTimestamp
+ }
+ return 0
+}
+
+func (x *OrderCompletedRequest) GetPayload() *OrderCreated {
+ if x != nil {
+ return x.Payload
+ }
+ return nil
+}
+
// -----------------------------------------------------------------------------
-// CartState represents the full cart snapshot returned by state/mutation replies.
-// Replaces the previous raw JSON payload.
+// Cart state snapshot (unchanged from v1 except envelope removal context)
// -----------------------------------------------------------------------------
type CartState struct {
state protoimpl.MessageState `protogen:"open.v1"`
@@ -411,7 +875,7 @@ type CartState struct {
func (x *CartState) Reset() {
*x = CartState{}
- mi := &file_cart_actor_proto_msgTypes[3]
+ mi := &file_cart_actor_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -423,7 +887,7 @@ func (x *CartState) String() string {
func (*CartState) ProtoMessage() {}
func (x *CartState) ProtoReflect() protoreflect.Message {
- mi := &file_cart_actor_proto_msgTypes[3]
+ mi := &file_cart_actor_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -436,7 +900,7 @@ func (x *CartState) ProtoReflect() protoreflect.Message {
// Deprecated: Use CartState.ProtoReflect.Descriptor instead.
func (*CartState) Descriptor() ([]byte, []int) {
- return file_cart_actor_proto_rawDescGZIP(), []int{3}
+ return file_cart_actor_proto_rawDescGZIP(), []int{13}
}
func (x *CartState) GetCartId() string {
@@ -502,7 +966,6 @@ func (x *CartState) GetPaymentStatus() string {
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"`
@@ -536,7 +999,7 @@ type CartItemState struct {
func (x *CartItemState) Reset() {
*x = CartItemState{}
- mi := &file_cart_actor_proto_msgTypes[4]
+ mi := &file_cart_actor_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -548,7 +1011,7 @@ func (x *CartItemState) String() string {
func (*CartItemState) ProtoMessage() {}
func (x *CartItemState) ProtoReflect() protoreflect.Message {
- mi := &file_cart_actor_proto_msgTypes[4]
+ mi := &file_cart_actor_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -561,7 +1024,7 @@ func (x *CartItemState) ProtoReflect() protoreflect.Message {
// Deprecated: Use CartItemState.ProtoReflect.Descriptor instead.
func (*CartItemState) Descriptor() ([]byte, []int) {
- return file_cart_actor_proto_rawDescGZIP(), []int{4}
+ return file_cart_actor_proto_rawDescGZIP(), []int{14}
}
func (x *CartItemState) GetId() int64 {
@@ -739,21 +1202,20 @@ func (x *CartItemState) GetStock() int32 {
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"`
+ PickupPoint *PickupPoint `protobuf:"bytes,5,opt,name=pickup_point,json=pickupPoint,proto3" json:"pickup_point,omitempty"` // Defined in messages.proto
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeliveryState) Reset() {
*x = DeliveryState{}
- mi := &file_cart_actor_proto_msgTypes[5]
+ mi := &file_cart_actor_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -765,7 +1227,7 @@ func (x *DeliveryState) String() string {
func (*DeliveryState) ProtoMessage() {}
func (x *DeliveryState) ProtoReflect() protoreflect.Message {
- mi := &file_cart_actor_proto_msgTypes[5]
+ mi := &file_cart_actor_proto_msgTypes[15]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -778,7 +1240,7 @@ func (x *DeliveryState) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeliveryState.ProtoReflect.Descriptor instead.
func (*DeliveryState) Descriptor() ([]byte, []int) {
- return file_cart_actor_proto_rawDescGZIP(), []int{5}
+ return file_cart_actor_proto_rawDescGZIP(), []int{15}
}
func (x *DeliveryState) GetId() int64 {
@@ -816,128 +1278,77 @@ func (x *DeliveryState) GetPickupPoint() *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"`
- // 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[6]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
-}
-
-func (x *StateReply) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*StateReply) ProtoMessage() {}
-
-func (x *StateReply) ProtoReflect() protoreflect.Message {
- mi := &file_cart_actor_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 StateReply.ProtoReflect.Descriptor instead.
-func (*StateReply) Descriptor() ([]byte, []int) {
- return file_cart_actor_proto_rawDescGZIP(), []int{6}
-}
-
-func (x *StateReply) GetStatusCode() int32 {
- if x != nil {
- return x.StatusCode
- }
- return 0
-}
-
-func (x *StateReply) GetResult() isStateReply_Result {
- if x != nil {
- 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\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" +
+ "\x10cart_actor.proto\x12\bmessages\x1a\x0emessages.proto\"\xae\x01\n" +
+ "\x11CartMutationReply\x12\x1f\n" +
+ "\vstatus_code\x18\x01 \x01(\x05R\n" +
+ "statusCode\x12+\n" +
+ "\x05state\x18\x02 \x01(\v2\x13.messages.CartStateH\x00R\x05state\x12\x16\n" +
+ "\x05error\x18\x03 \x01(\tH\x00R\x05error\x12)\n" +
+ "\x10server_timestamp\x18\x04 \x01(\x03R\x0fserverTimestampB\b\n" +
+ "\x06result\"'\n" +
+ "\fStateRequest\x12\x17\n" +
+ "\acart_id\x18\x01 \x01(\tR\x06cartId\"|\n" +
"\n" +
- "\bmutation\"\x7f\n" +
- "\rMutationReply\x12\x1f\n" +
+ "StateReply\x12\x1f\n" +
"\vstatus_code\x18\x01 \x01(\x05R\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\"\xf1\x02\n" +
+ "\x06result\"\x87\x01\n" +
+ "\x11AddRequestRequest\x12\x17\n" +
+ "\acart_id\x18\x01 \x01(\tR\x06cartId\x12)\n" +
+ "\x10client_timestamp\x18\x02 \x01(\x03R\x0fclientTimestamp\x12.\n" +
+ "\apayload\x18\n" +
+ " \x01(\v2\x14.messages.AddRequestR\apayload\"\x81\x01\n" +
+ "\x0eAddItemRequest\x12\x17\n" +
+ "\acart_id\x18\x01 \x01(\tR\x06cartId\x12)\n" +
+ "\x10client_timestamp\x18\x02 \x01(\x03R\x0fclientTimestamp\x12+\n" +
+ "\apayload\x18\n" +
+ " \x01(\v2\x11.messages.AddItemR\apayload\"\x87\x01\n" +
+ "\x11RemoveItemRequest\x12\x17\n" +
+ "\acart_id\x18\x01 \x01(\tR\x06cartId\x12)\n" +
+ "\x10client_timestamp\x18\x02 \x01(\x03R\x0fclientTimestamp\x12.\n" +
+ "\apayload\x18\n" +
+ " \x01(\v2\x14.messages.RemoveItemR\apayload\"\x8f\x01\n" +
+ "\x15RemoveDeliveryRequest\x12\x17\n" +
+ "\acart_id\x18\x01 \x01(\tR\x06cartId\x12)\n" +
+ "\x10client_timestamp\x18\x02 \x01(\x03R\x0fclientTimestamp\x122\n" +
+ "\apayload\x18\n" +
+ " \x01(\v2\x18.messages.RemoveDeliveryR\apayload\"\x8f\x01\n" +
+ "\x15ChangeQuantityRequest\x12\x17\n" +
+ "\acart_id\x18\x01 \x01(\tR\x06cartId\x12)\n" +
+ "\x10client_timestamp\x18\x02 \x01(\x03R\x0fclientTimestamp\x122\n" +
+ "\apayload\x18\n" +
+ " \x01(\v2\x18.messages.ChangeQuantityR\apayload\"\x89\x01\n" +
+ "\x12SetDeliveryRequest\x12\x17\n" +
+ "\acart_id\x18\x01 \x01(\tR\x06cartId\x12)\n" +
+ "\x10client_timestamp\x18\x02 \x01(\x03R\x0fclientTimestamp\x12/\n" +
+ "\apayload\x18\n" +
+ " \x01(\v2\x15.messages.SetDeliveryR\apayload\"\x8f\x01\n" +
+ "\x15SetPickupPointRequest\x12\x17\n" +
+ "\acart_id\x18\x01 \x01(\tR\x06cartId\x12)\n" +
+ "\x10client_timestamp\x18\x02 \x01(\x03R\x0fclientTimestamp\x122\n" +
+ "\apayload\x18\n" +
+ " \x01(\v2\x18.messages.SetPickupPointR\apayload\"\x99\x01\n" +
+ "\x1aCreateCheckoutOrderRequest\x12\x17\n" +
+ "\acart_id\x18\x01 \x01(\tR\x06cartId\x12)\n" +
+ "\x10client_timestamp\x18\x02 \x01(\x03R\x0fclientTimestamp\x127\n" +
+ "\apayload\x18\n" +
+ " \x01(\v2\x1d.messages.CreateCheckoutOrderR\apayload\"\x8d\x01\n" +
+ "\x13SetCartItemsRequest\x12\x17\n" +
+ "\acart_id\x18\x01 \x01(\tR\x06cartId\x12)\n" +
+ "\x10client_timestamp\x18\x02 \x01(\x03R\x0fclientTimestamp\x122\n" +
+ "\apayload\x18\n" +
+ " \x01(\v2\x18.messages.SetCartRequestR\apayload\"\x8d\x01\n" +
+ "\x15OrderCompletedRequest\x12\x17\n" +
+ "\acart_id\x18\x01 \x01(\tR\x06cartId\x12)\n" +
+ "\x10client_timestamp\x18\x02 \x01(\x03R\x0fclientTimestamp\x120\n" +
+ "\apayload\x18\n" +
+ " \x01(\v2\x16.messages.OrderCreatedR\apayload\"\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" +
@@ -988,16 +1399,19 @@ const file_cart_actor_proto_rawDesc = "" +
"\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" +
+ "\fpickup_point\x18\x05 \x01(\v2\x15.messages.PickupPointR\vpickupPoint2\xed\x05\n" +
+ "\tCartActor\x12F\n" +
"\n" +
- "StateReply\x12\x1f\n" +
- "\vstatus_code\x18\x01 \x01(\x05R\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" +
- "\x06result2\x84\x01\n" +
- "\tCartActor\x12=\n" +
- "\x06Mutate\x12\x1a.messages.MutationEnvelope\x1a\x17.messages.MutationReply\x128\n" +
+ "AddRequest\x12\x1b.messages.AddRequestRequest\x1a\x1b.messages.CartMutationReply\x12@\n" +
+ "\aAddItem\x12\x18.messages.AddItemRequest\x1a\x1b.messages.CartMutationReply\x12F\n" +
+ "\n" +
+ "RemoveItem\x12\x1b.messages.RemoveItemRequest\x1a\x1b.messages.CartMutationReply\x12N\n" +
+ "\x0eRemoveDelivery\x12\x1f.messages.RemoveDeliveryRequest\x1a\x1b.messages.CartMutationReply\x12N\n" +
+ "\x0eChangeQuantity\x12\x1f.messages.ChangeQuantityRequest\x1a\x1b.messages.CartMutationReply\x12H\n" +
+ "\vSetDelivery\x12\x1c.messages.SetDeliveryRequest\x1a\x1b.messages.CartMutationReply\x12N\n" +
+ "\x0eSetPickupPoint\x12\x1f.messages.SetPickupPointRequest\x1a\x1b.messages.CartMutationReply\x12J\n" +
+ "\fSetCartItems\x12\x1d.messages.SetCartItemsRequest\x1a\x1b.messages.CartMutationReply\x12N\n" +
+ "\x0eOrderCompleted\x12\x1f.messages.OrderCompletedRequest\x1a\x1b.messages.CartMutationReply\x128\n" +
"\bGetState\x12\x16.messages.StateRequest\x1a\x14.messages.StateReplyB.Z,git.tornberg.me/go-cart-actor/proto;messagesb\x06proto3"
var (
@@ -1012,49 +1426,74 @@ func file_cart_actor_proto_rawDescGZIP() []byte {
return file_cart_actor_proto_rawDescData
}
-var file_cart_actor_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
+var file_cart_actor_proto_msgTypes = make([]protoimpl.MessageInfo, 16)
var file_cart_actor_proto_goTypes = []any{
- (*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
+ (*CartMutationReply)(nil), // 0: messages.CartMutationReply
+ (*StateRequest)(nil), // 1: messages.StateRequest
+ (*StateReply)(nil), // 2: messages.StateReply
+ (*AddRequestRequest)(nil), // 3: messages.AddRequestRequest
+ (*AddItemRequest)(nil), // 4: messages.AddItemRequest
+ (*RemoveItemRequest)(nil), // 5: messages.RemoveItemRequest
+ (*RemoveDeliveryRequest)(nil), // 6: messages.RemoveDeliveryRequest
+ (*ChangeQuantityRequest)(nil), // 7: messages.ChangeQuantityRequest
+ (*SetDeliveryRequest)(nil), // 8: messages.SetDeliveryRequest
+ (*SetPickupPointRequest)(nil), // 9: messages.SetPickupPointRequest
+ (*CreateCheckoutOrderRequest)(nil), // 10: messages.CreateCheckoutOrderRequest
+ (*SetCartItemsRequest)(nil), // 11: messages.SetCartItemsRequest
+ (*OrderCompletedRequest)(nil), // 12: messages.OrderCompletedRequest
+ (*CartState)(nil), // 13: messages.CartState
+ (*CartItemState)(nil), // 14: messages.CartItemState
+ (*DeliveryState)(nil), // 15: messages.DeliveryState
+ (*AddRequest)(nil), // 16: messages.AddRequest
+ (*AddItem)(nil), // 17: messages.AddItem
+ (*RemoveItem)(nil), // 18: messages.RemoveItem
+ (*RemoveDelivery)(nil), // 19: messages.RemoveDelivery
+ (*ChangeQuantity)(nil), // 20: messages.ChangeQuantity
+ (*SetDelivery)(nil), // 21: messages.SetDelivery
+ (*SetPickupPoint)(nil), // 22: messages.SetPickupPoint
+ (*CreateCheckoutOrder)(nil), // 23: messages.CreateCheckoutOrder
+ (*SetCartRequest)(nil), // 24: messages.SetCartRequest
+ (*OrderCreated)(nil), // 25: messages.OrderCreated
+ (*PickupPoint)(nil), // 26: messages.PickupPoint
}
var file_cart_actor_proto_depIdxs = []int32{
- 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
+ 13, // 0: messages.CartMutationReply.state:type_name -> messages.CartState
+ 13, // 1: messages.StateReply.state:type_name -> messages.CartState
+ 16, // 2: messages.AddRequestRequest.payload:type_name -> messages.AddRequest
+ 17, // 3: messages.AddItemRequest.payload:type_name -> messages.AddItem
+ 18, // 4: messages.RemoveItemRequest.payload:type_name -> messages.RemoveItem
+ 19, // 5: messages.RemoveDeliveryRequest.payload:type_name -> messages.RemoveDelivery
+ 20, // 6: messages.ChangeQuantityRequest.payload:type_name -> messages.ChangeQuantity
+ 21, // 7: messages.SetDeliveryRequest.payload:type_name -> messages.SetDelivery
+ 22, // 8: messages.SetPickupPointRequest.payload:type_name -> messages.SetPickupPoint
+ 23, // 9: messages.CreateCheckoutOrderRequest.payload:type_name -> messages.CreateCheckoutOrder
+ 24, // 10: messages.SetCartItemsRequest.payload:type_name -> messages.SetCartRequest
+ 25, // 11: messages.OrderCompletedRequest.payload:type_name -> messages.OrderCreated
+ 14, // 12: messages.CartState.items:type_name -> messages.CartItemState
+ 15, // 13: messages.CartState.deliveries:type_name -> messages.DeliveryState
+ 26, // 14: messages.DeliveryState.pickup_point:type_name -> messages.PickupPoint
+ 3, // 15: messages.CartActor.AddRequest:input_type -> messages.AddRequestRequest
+ 4, // 16: messages.CartActor.AddItem:input_type -> messages.AddItemRequest
+ 5, // 17: messages.CartActor.RemoveItem:input_type -> messages.RemoveItemRequest
+ 6, // 18: messages.CartActor.RemoveDelivery:input_type -> messages.RemoveDeliveryRequest
+ 7, // 19: messages.CartActor.ChangeQuantity:input_type -> messages.ChangeQuantityRequest
+ 8, // 20: messages.CartActor.SetDelivery:input_type -> messages.SetDeliveryRequest
+ 9, // 21: messages.CartActor.SetPickupPoint:input_type -> messages.SetPickupPointRequest
+ 11, // 22: messages.CartActor.SetCartItems:input_type -> messages.SetCartItemsRequest
+ 12, // 23: messages.CartActor.OrderCompleted:input_type -> messages.OrderCompletedRequest
+ 1, // 24: messages.CartActor.GetState:input_type -> messages.StateRequest
+ 0, // 25: messages.CartActor.AddRequest:output_type -> messages.CartMutationReply
+ 0, // 26: messages.CartActor.AddItem:output_type -> messages.CartMutationReply
+ 0, // 27: messages.CartActor.RemoveItem:output_type -> messages.CartMutationReply
+ 0, // 28: messages.CartActor.RemoveDelivery:output_type -> messages.CartMutationReply
+ 0, // 29: messages.CartActor.ChangeQuantity:output_type -> messages.CartMutationReply
+ 0, // 30: messages.CartActor.SetDelivery:output_type -> messages.CartMutationReply
+ 0, // 31: messages.CartActor.SetPickupPoint:output_type -> messages.CartMutationReply
+ 0, // 32: messages.CartActor.SetCartItems:output_type -> messages.CartMutationReply
+ 0, // 33: messages.CartActor.OrderCompleted:output_type -> messages.CartMutationReply
+ 2, // 34: messages.CartActor.GetState:output_type -> messages.StateReply
+ 25, // [25:35] is the sub-list for method output_type
+ 15, // [15:25] 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
@@ -1067,22 +1506,10 @@ func file_cart_actor_proto_init() {
}
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),
+ (*CartMutationReply_State)(nil),
+ (*CartMutationReply_Error)(nil),
}
- file_cart_actor_proto_msgTypes[1].OneofWrappers = []any{
- (*MutationReply_State)(nil),
- (*MutationReply_Error)(nil),
- }
- file_cart_actor_proto_msgTypes[6].OneofWrappers = []any{
+ file_cart_actor_proto_msgTypes[2].OneofWrappers = []any{
(*StateReply_State)(nil),
(*StateReply_Error)(nil),
}
@@ -1092,7 +1519,7 @@ func file_cart_actor_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_cart_actor_proto_rawDesc), len(file_cart_actor_proto_rawDesc)),
NumEnums: 0,
- NumMessages: 7,
+ NumMessages: 16,
NumExtensions: 0,
NumServices: 1,
},
diff --git a/proto/cart_actor.proto b/proto/cart_actor.proto
index b09a1c2..9da8b8d 100644
--- a/proto/cart_actor.proto
+++ b/proto/cart_actor.proto
@@ -3,66 +3,114 @@ syntax = "proto3";
package messages;
option go_package = "git.tornberg.me/go-cart-actor/proto;messages";
+
import "messages.proto";
// -----------------------------------------------------------------------------
-// Cart Actor gRPC API (oneof Envelope Variant)
+// Cart Actor gRPC API (Breaking v2 - Per-Mutation RPCs)
// -----------------------------------------------------------------------------
-// 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.
+// This version removes the previous MutationEnvelope + Mutate RPC.
+// Each mutation now has its own request wrapper and dedicated RPC method
+// providing simpler, type-focused client stubs and enabling per-mutation
+// metrics, auth and rate limiting.
//
-// NOTE: Regenerate Go code after editing:
+// 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
+//
+// Backward compatibility: This is a breaking change (old clients must update).
// -----------------------------------------------------------------------------
-// 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;
+// Shared reply for all mutation RPCs.
+message CartMutationReply {
+ int32 status_code = 1; // HTTP-like status (200 success, 4xx client, 5xx server)
+ oneof result {
+ CartState state = 2; // Updated cart state on success
+ string error = 3; // Error message on failure
}
+ int64 server_timestamp = 4; // Server-assigned Unix timestamp (optional auditing)
}
-// 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;
+// Fetch current cart state without mutation.
+message StateRequest {
+ string cart_id = 1;
+}
- // Exactly one of state or error will be set.
+message StateReply {
+ int32 status_code = 1;
oneof result {
CartState state = 2;
string error = 3;
}
}
-// StateRequest fetches current cart state without mutating.
-message StateRequest {
+// Per-mutation request wrappers. We wrap the existing inner mutation
+// messages (defined in messages.proto) to add cart_id + optional metadata
+// without altering the inner message definitions.
+
+message AddRequestRequest {
string cart_id = 1;
+ int64 client_timestamp = 2;
+ AddRequest payload = 10;
+}
+
+message AddItemRequest {
+ string cart_id = 1;
+ int64 client_timestamp = 2;
+ AddItem payload = 10;
+}
+
+message RemoveItemRequest {
+ string cart_id = 1;
+ int64 client_timestamp = 2;
+ RemoveItem payload = 10;
+}
+
+message RemoveDeliveryRequest {
+ string cart_id = 1;
+ int64 client_timestamp = 2;
+ RemoveDelivery payload = 10;
+}
+
+message ChangeQuantityRequest {
+ string cart_id = 1;
+ int64 client_timestamp = 2;
+ ChangeQuantity payload = 10;
+}
+
+message SetDeliveryRequest {
+ string cart_id = 1;
+ int64 client_timestamp = 2;
+ SetDelivery payload = 10;
+}
+
+message SetPickupPointRequest {
+ string cart_id = 1;
+ int64 client_timestamp = 2;
+ SetPickupPoint payload = 10;
+}
+
+message CreateCheckoutOrderRequest {
+ string cart_id = 1;
+ int64 client_timestamp = 2;
+ CreateCheckoutOrder payload = 10;
+}
+
+message SetCartItemsRequest {
+ string cart_id = 1;
+ int64 client_timestamp = 2;
+ SetCartRequest payload = 10;
+}
+
+message OrderCompletedRequest {
+ string cart_id = 1;
+ int64 client_timestamp = 2;
+ OrderCreated payload = 10;
}
// -----------------------------------------------------------------------------
-// CartState represents the full cart snapshot returned by state/mutation replies.
-// Replaces the previous raw JSON payload.
+// Cart state snapshot (unchanged from v1 except envelope removal context)
// -----------------------------------------------------------------------------
message CartState {
string cart_id = 1;
@@ -76,7 +124,6 @@ message CartState {
string payment_status = 9;
}
-// Lightweight representation of an item in the cart
message CartItemState {
int64 id = 1;
int64 source_item_id = 2;
@@ -105,38 +152,37 @@ message CartItemState {
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;
+ PickupPoint pickup_point = 5; // Defined in messages.proto
}
-// StateReply mirrors MutationReply for consistency.
-message StateReply {
- int32 status_code = 1;
+// (CheckoutRequest / CheckoutReply removed - checkout handled at HTTP layer)
- oneof result {
- CartState state = 2;
- string error = 3;
- }
-}
-
-// CartActor exposes mutation and state retrieval for remote grains.
+// -----------------------------------------------------------------------------
+// Service definition (per-mutation RPCs + checkout)
+// -----------------------------------------------------------------------------
service CartActor {
- // Mutate applies a single mutation to a cart, creating the cart lazily if needed.
- rpc Mutate(MutationEnvelope) returns (MutationReply);
+ rpc AddRequest(AddRequestRequest) returns (CartMutationReply);
+ rpc AddItem(AddItemRequest) returns (CartMutationReply);
+ rpc RemoveItem(RemoveItemRequest) returns (CartMutationReply);
+ rpc RemoveDelivery(RemoveDeliveryRequest) returns (CartMutationReply);
+ rpc ChangeQuantity(ChangeQuantityRequest) returns (CartMutationReply);
+ rpc SetDelivery(SetDeliveryRequest) returns (CartMutationReply);
+ rpc SetPickupPoint(SetPickupPointRequest) returns (CartMutationReply);
+ // (Checkout RPC removed - handled externally)
+ rpc SetCartItems(SetCartItemsRequest) returns (CartMutationReply);
+ rpc OrderCompleted(OrderCompletedRequest) returns (CartMutationReply);
- // GetState retrieves the cart's current state (JSON).
rpc GetState(StateRequest) returns (StateReply);
}
// -----------------------------------------------------------------------------
-// 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.
+// Future enhancements:
+// * BatchMutate RPC (repeated heterogeneous mutations)
+// * Streaming state updates (WatchState)
+// * Versioning / optimistic concurrency control
// -----------------------------------------------------------------------------
diff --git a/proto/cart_actor_grpc.pb.go b/proto/cart_actor_grpc.pb.go
index 247f758..bd34ce3 100644
--- a/proto/cart_actor_grpc.pb.go
+++ b/proto/cart_actor_grpc.pb.go
@@ -19,19 +19,36 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
- CartActor_Mutate_FullMethodName = "/messages.CartActor/Mutate"
- CartActor_GetState_FullMethodName = "/messages.CartActor/GetState"
+ CartActor_AddRequest_FullMethodName = "/messages.CartActor/AddRequest"
+ CartActor_AddItem_FullMethodName = "/messages.CartActor/AddItem"
+ CartActor_RemoveItem_FullMethodName = "/messages.CartActor/RemoveItem"
+ CartActor_RemoveDelivery_FullMethodName = "/messages.CartActor/RemoveDelivery"
+ CartActor_ChangeQuantity_FullMethodName = "/messages.CartActor/ChangeQuantity"
+ CartActor_SetDelivery_FullMethodName = "/messages.CartActor/SetDelivery"
+ CartActor_SetPickupPoint_FullMethodName = "/messages.CartActor/SetPickupPoint"
+ CartActor_SetCartItems_FullMethodName = "/messages.CartActor/SetCartItems"
+ CartActor_OrderCompleted_FullMethodName = "/messages.CartActor/OrderCompleted"
+ CartActor_GetState_FullMethodName = "/messages.CartActor/GetState"
)
// CartActorClient is the client API for CartActor service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
-// CartActor exposes mutation and state retrieval for remote grains.
+// -----------------------------------------------------------------------------
+// Service definition (per-mutation RPCs + checkout)
+// -----------------------------------------------------------------------------
type CartActorClient interface {
- // Mutate applies a single mutation to a cart, creating the cart lazily if needed.
- Mutate(ctx context.Context, in *MutationEnvelope, opts ...grpc.CallOption) (*MutationReply, error)
- // GetState retrieves the cart's current state (JSON).
+ AddRequest(ctx context.Context, in *AddRequestRequest, opts ...grpc.CallOption) (*CartMutationReply, error)
+ AddItem(ctx context.Context, in *AddItemRequest, opts ...grpc.CallOption) (*CartMutationReply, error)
+ RemoveItem(ctx context.Context, in *RemoveItemRequest, opts ...grpc.CallOption) (*CartMutationReply, error)
+ RemoveDelivery(ctx context.Context, in *RemoveDeliveryRequest, opts ...grpc.CallOption) (*CartMutationReply, error)
+ ChangeQuantity(ctx context.Context, in *ChangeQuantityRequest, opts ...grpc.CallOption) (*CartMutationReply, error)
+ SetDelivery(ctx context.Context, in *SetDeliveryRequest, opts ...grpc.CallOption) (*CartMutationReply, error)
+ SetPickupPoint(ctx context.Context, in *SetPickupPointRequest, opts ...grpc.CallOption) (*CartMutationReply, error)
+ // (Checkout RPC removed - handled externally)
+ SetCartItems(ctx context.Context, in *SetCartItemsRequest, opts ...grpc.CallOption) (*CartMutationReply, error)
+ OrderCompleted(ctx context.Context, in *OrderCompletedRequest, opts ...grpc.CallOption) (*CartMutationReply, error)
GetState(ctx context.Context, in *StateRequest, opts ...grpc.CallOption) (*StateReply, error)
}
@@ -43,10 +60,90 @@ func NewCartActorClient(cc grpc.ClientConnInterface) CartActorClient {
return &cartActorClient{cc}
}
-func (c *cartActorClient) Mutate(ctx context.Context, in *MutationEnvelope, opts ...grpc.CallOption) (*MutationReply, error) {
+func (c *cartActorClient) AddRequest(ctx context.Context, in *AddRequestRequest, opts ...grpc.CallOption) (*CartMutationReply, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
- out := new(MutationReply)
- err := c.cc.Invoke(ctx, CartActor_Mutate_FullMethodName, in, out, cOpts...)
+ out := new(CartMutationReply)
+ err := c.cc.Invoke(ctx, CartActor_AddRequest_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *cartActorClient) AddItem(ctx context.Context, in *AddItemRequest, opts ...grpc.CallOption) (*CartMutationReply, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(CartMutationReply)
+ err := c.cc.Invoke(ctx, CartActor_AddItem_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *cartActorClient) RemoveItem(ctx context.Context, in *RemoveItemRequest, opts ...grpc.CallOption) (*CartMutationReply, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(CartMutationReply)
+ err := c.cc.Invoke(ctx, CartActor_RemoveItem_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *cartActorClient) RemoveDelivery(ctx context.Context, in *RemoveDeliveryRequest, opts ...grpc.CallOption) (*CartMutationReply, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(CartMutationReply)
+ err := c.cc.Invoke(ctx, CartActor_RemoveDelivery_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *cartActorClient) ChangeQuantity(ctx context.Context, in *ChangeQuantityRequest, opts ...grpc.CallOption) (*CartMutationReply, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(CartMutationReply)
+ err := c.cc.Invoke(ctx, CartActor_ChangeQuantity_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *cartActorClient) SetDelivery(ctx context.Context, in *SetDeliveryRequest, opts ...grpc.CallOption) (*CartMutationReply, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(CartMutationReply)
+ err := c.cc.Invoke(ctx, CartActor_SetDelivery_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *cartActorClient) SetPickupPoint(ctx context.Context, in *SetPickupPointRequest, opts ...grpc.CallOption) (*CartMutationReply, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(CartMutationReply)
+ err := c.cc.Invoke(ctx, CartActor_SetPickupPoint_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *cartActorClient) SetCartItems(ctx context.Context, in *SetCartItemsRequest, opts ...grpc.CallOption) (*CartMutationReply, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(CartMutationReply)
+ err := c.cc.Invoke(ctx, CartActor_SetCartItems_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *cartActorClient) OrderCompleted(ctx context.Context, in *OrderCompletedRequest, opts ...grpc.CallOption) (*CartMutationReply, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(CartMutationReply)
+ err := c.cc.Invoke(ctx, CartActor_OrderCompleted_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
@@ -67,11 +164,20 @@ func (c *cartActorClient) GetState(ctx context.Context, in *StateRequest, opts .
// All implementations must embed UnimplementedCartActorServer
// for forward compatibility.
//
-// CartActor exposes mutation and state retrieval for remote grains.
+// -----------------------------------------------------------------------------
+// Service definition (per-mutation RPCs + checkout)
+// -----------------------------------------------------------------------------
type CartActorServer interface {
- // Mutate applies a single mutation to a cart, creating the cart lazily if needed.
- Mutate(context.Context, *MutationEnvelope) (*MutationReply, error)
- // GetState retrieves the cart's current state (JSON).
+ AddRequest(context.Context, *AddRequestRequest) (*CartMutationReply, error)
+ AddItem(context.Context, *AddItemRequest) (*CartMutationReply, error)
+ RemoveItem(context.Context, *RemoveItemRequest) (*CartMutationReply, error)
+ RemoveDelivery(context.Context, *RemoveDeliveryRequest) (*CartMutationReply, error)
+ ChangeQuantity(context.Context, *ChangeQuantityRequest) (*CartMutationReply, error)
+ SetDelivery(context.Context, *SetDeliveryRequest) (*CartMutationReply, error)
+ SetPickupPoint(context.Context, *SetPickupPointRequest) (*CartMutationReply, error)
+ // (Checkout RPC removed - handled externally)
+ SetCartItems(context.Context, *SetCartItemsRequest) (*CartMutationReply, error)
+ OrderCompleted(context.Context, *OrderCompletedRequest) (*CartMutationReply, error)
GetState(context.Context, *StateRequest) (*StateReply, error)
mustEmbedUnimplementedCartActorServer()
}
@@ -83,8 +189,32 @@ type CartActorServer interface {
// pointer dereference when methods are called.
type UnimplementedCartActorServer struct{}
-func (UnimplementedCartActorServer) Mutate(context.Context, *MutationEnvelope) (*MutationReply, error) {
- return nil, status.Errorf(codes.Unimplemented, "method Mutate not implemented")
+func (UnimplementedCartActorServer) AddRequest(context.Context, *AddRequestRequest) (*CartMutationReply, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method AddRequest not implemented")
+}
+func (UnimplementedCartActorServer) AddItem(context.Context, *AddItemRequest) (*CartMutationReply, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method AddItem not implemented")
+}
+func (UnimplementedCartActorServer) RemoveItem(context.Context, *RemoveItemRequest) (*CartMutationReply, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method RemoveItem not implemented")
+}
+func (UnimplementedCartActorServer) RemoveDelivery(context.Context, *RemoveDeliveryRequest) (*CartMutationReply, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method RemoveDelivery not implemented")
+}
+func (UnimplementedCartActorServer) ChangeQuantity(context.Context, *ChangeQuantityRequest) (*CartMutationReply, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method ChangeQuantity not implemented")
+}
+func (UnimplementedCartActorServer) SetDelivery(context.Context, *SetDeliveryRequest) (*CartMutationReply, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method SetDelivery not implemented")
+}
+func (UnimplementedCartActorServer) SetPickupPoint(context.Context, *SetPickupPointRequest) (*CartMutationReply, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method SetPickupPoint not implemented")
+}
+func (UnimplementedCartActorServer) SetCartItems(context.Context, *SetCartItemsRequest) (*CartMutationReply, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method SetCartItems not implemented")
+}
+func (UnimplementedCartActorServer) OrderCompleted(context.Context, *OrderCompletedRequest) (*CartMutationReply, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method OrderCompleted not implemented")
}
func (UnimplementedCartActorServer) GetState(context.Context, *StateRequest) (*StateReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetState not implemented")
@@ -110,20 +240,164 @@ func RegisterCartActorServer(s grpc.ServiceRegistrar, srv CartActorServer) {
s.RegisterService(&CartActor_ServiceDesc, srv)
}
-func _CartActor_Mutate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
- in := new(MutationEnvelope)
+func _CartActor_AddRequest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(AddRequestRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
- return srv.(CartActorServer).Mutate(ctx, in)
+ return srv.(CartActorServer).AddRequest(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
- FullMethod: CartActor_Mutate_FullMethodName,
+ FullMethod: CartActor_AddRequest_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
- return srv.(CartActorServer).Mutate(ctx, req.(*MutationEnvelope))
+ return srv.(CartActorServer).AddRequest(ctx, req.(*AddRequestRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _CartActor_AddItem_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(AddItemRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(CartActorServer).AddItem(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: CartActor_AddItem_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(CartActorServer).AddItem(ctx, req.(*AddItemRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _CartActor_RemoveItem_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(RemoveItemRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(CartActorServer).RemoveItem(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: CartActor_RemoveItem_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(CartActorServer).RemoveItem(ctx, req.(*RemoveItemRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _CartActor_RemoveDelivery_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(RemoveDeliveryRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(CartActorServer).RemoveDelivery(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: CartActor_RemoveDelivery_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(CartActorServer).RemoveDelivery(ctx, req.(*RemoveDeliveryRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _CartActor_ChangeQuantity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(ChangeQuantityRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(CartActorServer).ChangeQuantity(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: CartActor_ChangeQuantity_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(CartActorServer).ChangeQuantity(ctx, req.(*ChangeQuantityRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _CartActor_SetDelivery_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(SetDeliveryRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(CartActorServer).SetDelivery(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: CartActor_SetDelivery_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(CartActorServer).SetDelivery(ctx, req.(*SetDeliveryRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _CartActor_SetPickupPoint_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(SetPickupPointRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(CartActorServer).SetPickupPoint(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: CartActor_SetPickupPoint_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(CartActorServer).SetPickupPoint(ctx, req.(*SetPickupPointRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _CartActor_SetCartItems_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(SetCartItemsRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(CartActorServer).SetCartItems(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: CartActor_SetCartItems_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(CartActorServer).SetCartItems(ctx, req.(*SetCartItemsRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _CartActor_OrderCompleted_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(OrderCompletedRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(CartActorServer).OrderCompleted(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: CartActor_OrderCompleted_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(CartActorServer).OrderCompleted(ctx, req.(*OrderCompletedRequest))
}
return interceptor(ctx, in, info, handler)
}
@@ -154,8 +428,40 @@ var CartActor_ServiceDesc = grpc.ServiceDesc{
HandlerType: (*CartActorServer)(nil),
Methods: []grpc.MethodDesc{
{
- MethodName: "Mutate",
- Handler: _CartActor_Mutate_Handler,
+ MethodName: "AddRequest",
+ Handler: _CartActor_AddRequest_Handler,
+ },
+ {
+ MethodName: "AddItem",
+ Handler: _CartActor_AddItem_Handler,
+ },
+ {
+ MethodName: "RemoveItem",
+ Handler: _CartActor_RemoveItem_Handler,
+ },
+ {
+ MethodName: "RemoveDelivery",
+ Handler: _CartActor_RemoveDelivery_Handler,
+ },
+ {
+ MethodName: "ChangeQuantity",
+ Handler: _CartActor_ChangeQuantity_Handler,
+ },
+ {
+ MethodName: "SetDelivery",
+ Handler: _CartActor_SetDelivery_Handler,
+ },
+ {
+ MethodName: "SetPickupPoint",
+ Handler: _CartActor_SetPickupPoint_Handler,
+ },
+ {
+ MethodName: "SetCartItems",
+ Handler: _CartActor_SetCartItems_Handler,
+ },
+ {
+ MethodName: "OrderCompleted",
+ Handler: _CartActor_OrderCompleted_Handler,
},
{
MethodName: "GetState",
diff --git a/proto/messages.pb.go b/proto/messages.pb.go
index add0f30..506bb46 100644
--- a/proto/messages.pb.go
+++ b/proto/messages.pb.go
@@ -889,6 +889,102 @@ func (x *OrderCreated) GetStatus() string {
return ""
}
+type Noop struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *Noop) Reset() {
+ *x = Noop{}
+ mi := &file_messages_proto_msgTypes[11]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *Noop) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Noop) ProtoMessage() {}
+
+func (x *Noop) ProtoReflect() protoreflect.Message {
+ mi := &file_messages_proto_msgTypes[11]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Noop.ProtoReflect.Descriptor instead.
+func (*Noop) Descriptor() ([]byte, []int) {
+ return file_messages_proto_rawDescGZIP(), []int{11}
+}
+
+type InitializeCheckout 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"`
+ PaymentInProgress bool `protobuf:"varint,3,opt,name=paymentInProgress,proto3" json:"paymentInProgress,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *InitializeCheckout) Reset() {
+ *x = InitializeCheckout{}
+ mi := &file_messages_proto_msgTypes[12]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *InitializeCheckout) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*InitializeCheckout) ProtoMessage() {}
+
+func (x *InitializeCheckout) ProtoReflect() protoreflect.Message {
+ mi := &file_messages_proto_msgTypes[12]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use InitializeCheckout.ProtoReflect.Descriptor instead.
+func (*InitializeCheckout) Descriptor() ([]byte, []int) {
+ return file_messages_proto_rawDescGZIP(), []int{12}
+}
+
+func (x *InitializeCheckout) GetOrderId() string {
+ if x != nil {
+ return x.OrderId
+ }
+ return ""
+}
+
+func (x *InitializeCheckout) GetStatus() string {
+ if x != nil {
+ return x.Status
+ }
+ return ""
+}
+
+func (x *InitializeCheckout) GetPaymentInProgress() bool {
+ if x != nil {
+ return x.PaymentInProgress
+ }
+ return false
+}
+
var File_messages_proto protoreflect.FileDescriptor
const file_messages_proto_rawDesc = "" +
@@ -997,7 +1093,12 @@ const file_messages_proto_rawDesc = "" +
"\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.Z,git.tornberg.me/go-cart-actor/proto;messagesb\x06proto3"
+ "\x06status\x18\x02 \x01(\tR\x06status\"\x06\n" +
+ "\x04Noop\"t\n" +
+ "\x12InitializeCheckout\x12\x18\n" +
+ "\aorderId\x18\x01 \x01(\tR\aorderId\x12\x16\n" +
+ "\x06status\x18\x02 \x01(\tR\x06status\x12,\n" +
+ "\x11paymentInProgress\x18\x03 \x01(\bR\x11paymentInProgressB.Z,git.tornberg.me/go-cart-actor/proto;messagesb\x06proto3"
var (
file_messages_proto_rawDescOnce sync.Once
@@ -1011,7 +1112,7 @@ func file_messages_proto_rawDescGZIP() []byte {
return file_messages_proto_rawDescData
}
-var file_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
+var file_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
var file_messages_proto_goTypes = []any{
(*AddRequest)(nil), // 0: messages.AddRequest
(*SetCartRequest)(nil), // 1: messages.SetCartRequest
@@ -1024,6 +1125,8 @@ var file_messages_proto_goTypes = []any{
(*RemoveDelivery)(nil), // 8: messages.RemoveDelivery
(*CreateCheckoutOrder)(nil), // 9: messages.CreateCheckoutOrder
(*OrderCreated)(nil), // 10: messages.OrderCreated
+ (*Noop)(nil), // 11: messages.Noop
+ (*InitializeCheckout)(nil), // 12: messages.InitializeCheckout
}
var file_messages_proto_depIdxs = []int32{
0, // 0: messages.SetCartRequest.items:type_name -> messages.AddRequest
@@ -1051,7 +1154,7 @@ func file_messages_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_messages_proto_rawDesc), len(file_messages_proto_rawDesc)),
NumEnums: 0,
- NumMessages: 11,
+ NumMessages: 13,
NumExtensions: 0,
NumServices: 0,
},
diff --git a/proto/messages.proto b/proto/messages.proto
index 90f1008..52d09ab 100644
--- a/proto/messages.proto
+++ b/proto/messages.proto
@@ -93,3 +93,13 @@ message OrderCreated {
string orderId = 1;
string status = 2;
}
+
+message Noop {
+ // Intentionally empty - used for ownership acquisition or health pings
+}
+
+message InitializeCheckout {
+ string orderId = 1;
+ string status = 2;
+ bool paymentInProgress = 3;
+}
diff --git a/remote-grain-pool._go b/remote-grain-pool._go
deleted file mode 100644
index bc13b41..0000000
--- a/remote-grain-pool._go
+++ /dev/null
@@ -1,67 +0,0 @@
-// package main
-
-// import "sync"
-
-// type RemoteGrainPool struct {
-// mu sync.RWMutex
-// Host string
-// grains map[CartId]*RemoteGrain
-// }
-
-// func NewRemoteGrainPool(addr string) *RemoteGrainPool {
-// return &RemoteGrainPool{
-// Host: addr,
-// grains: make(map[CartId]*RemoteGrain),
-// }
-// }
-
-// func (p *RemoteGrainPool) findRemoteGrain(id CartId) *RemoteGrain {
-// p.mu.RLock()
-// grain, ok := p.grains[id]
-// p.mu.RUnlock()
-// if !ok {
-// return nil
-// }
-// return grain
-// }
-
-// func (p *RemoteGrainPool) findOrCreateGrain(id CartId) (*RemoteGrain, error) {
-// grain := p.findRemoteGrain(id)
-
-// if grain == nil {
-// grain, err := NewRemoteGrain(id, p.Host)
-// if err != nil {
-// return nil, err
-// }
-// p.mu.Lock()
-// p.grains[id] = grain
-// p.mu.Unlock()
-// }
-// return grain, nil
-// }
-
-// func (p *RemoteGrainPool) Delete(id CartId) {
-// p.mu.Lock()
-// delete(p.grains, id)
-// p.mu.Unlock()
-// }
-
-// func (p *RemoteGrainPool) Process(id CartId, messages ...Message) (*FrameWithPayload, error) {
-// var result *FrameWithPayload
-// grain, err := p.findOrCreateGrain(id)
-// if err != nil {
-// return nil, err
-// }
-// for _, message := range messages {
-// result, err = grain.HandleMessage(&message, false)
-// }
-// return result, err
-// }
-
-// func (p *RemoteGrainPool) Get(id CartId) (*FrameWithPayload, error) {
-// grain, err := p.findOrCreateGrain(id)
-// if err != nil {
-// return nil, err
-// }
-// return grain.GetCurrentState()
-// }
diff --git a/remote_grain_grpc.go b/remote_grain_grpc.go
index 81d7492..bcc530c 100644
--- a/remote_grain_grpc.go
+++ b/remote_grain_grpc.go
@@ -67,8 +67,8 @@ func (g *RemoteGrainGRPC) GetId() CartId {
return g.Id
}
-// Apply executes a cart mutation remotely using the typed oneof MutationEnvelope
-// and returns a *CartGrain reconstructed from the typed MutationReply (state oneof).
+// Apply executes a cart mutation via per-mutation RPCs (breaking v2 API)
+// and returns a *CartGrain reconstructed from the CartMutationReply state.
func (g *RemoteGrainGRPC) Apply(content interface{}, isReplay bool) (*CartGrain, error) {
if isReplay {
return nil, fmt.Errorf("replay not supported for remote grains")
@@ -78,45 +78,100 @@ func (g *RemoteGrainGRPC) Apply(content interface{}, isReplay bool) (*CartGrain,
}
ts := time.Now().Unix()
- env := &proto.MutationEnvelope{
- CartId: g.Id.String(),
- ClientTimestamp: ts,
- }
+
+ var invoke func(ctx context.Context) (*proto.CartMutationReply, error)
switch m := content.(type) {
case *proto.AddRequest:
- env.Mutation = &proto.MutationEnvelope_AddRequest{AddRequest: m}
+ invoke = func(ctx context.Context) (*proto.CartMutationReply, error) {
+ return g.client.AddRequest(ctx, &proto.AddRequestRequest{
+ CartId: g.Id.String(),
+ ClientTimestamp: ts,
+ Payload: m,
+ })
+ }
case *proto.AddItem:
- env.Mutation = &proto.MutationEnvelope_AddItem{AddItem: m}
+ invoke = func(ctx context.Context) (*proto.CartMutationReply, error) {
+ return g.client.AddItem(ctx, &proto.AddItemRequest{
+ CartId: g.Id.String(),
+ ClientTimestamp: ts,
+ Payload: m,
+ })
+ }
case *proto.RemoveItem:
- env.Mutation = &proto.MutationEnvelope_RemoveItem{RemoveItem: m}
+ invoke = func(ctx context.Context) (*proto.CartMutationReply, error) {
+ return g.client.RemoveItem(ctx, &proto.RemoveItemRequest{
+ CartId: g.Id.String(),
+ ClientTimestamp: ts,
+ Payload: m,
+ })
+ }
case *proto.RemoveDelivery:
- env.Mutation = &proto.MutationEnvelope_RemoveDelivery{RemoveDelivery: m}
+ invoke = func(ctx context.Context) (*proto.CartMutationReply, error) {
+ return g.client.RemoveDelivery(ctx, &proto.RemoveDeliveryRequest{
+ CartId: g.Id.String(),
+ ClientTimestamp: ts,
+ Payload: m,
+ })
+ }
case *proto.ChangeQuantity:
- env.Mutation = &proto.MutationEnvelope_ChangeQuantity{ChangeQuantity: m}
+ invoke = func(ctx context.Context) (*proto.CartMutationReply, error) {
+ return g.client.ChangeQuantity(ctx, &proto.ChangeQuantityRequest{
+ CartId: g.Id.String(),
+ ClientTimestamp: ts,
+ Payload: m,
+ })
+ }
case *proto.SetDelivery:
- env.Mutation = &proto.MutationEnvelope_SetDelivery{SetDelivery: m}
+ invoke = func(ctx context.Context) (*proto.CartMutationReply, error) {
+ return g.client.SetDelivery(ctx, &proto.SetDeliveryRequest{
+ CartId: g.Id.String(),
+ ClientTimestamp: ts,
+ Payload: m,
+ })
+ }
case *proto.SetPickupPoint:
- env.Mutation = &proto.MutationEnvelope_SetPickupPoint{SetPickupPoint: m}
+ invoke = func(ctx context.Context) (*proto.CartMutationReply, error) {
+ return g.client.SetPickupPoint(ctx, &proto.SetPickupPointRequest{
+ CartId: g.Id.String(),
+ ClientTimestamp: ts,
+ Payload: m,
+ })
+ }
case *proto.CreateCheckoutOrder:
- env.Mutation = &proto.MutationEnvelope_CreateCheckoutOrder{CreateCheckoutOrder: m}
+ return nil, fmt.Errorf("CreateCheckoutOrder deprecated: checkout is handled via HTTP endpoint (HandleCheckout)")
case *proto.SetCartRequest:
- env.Mutation = &proto.MutationEnvelope_SetCartItems{SetCartItems: m}
+ invoke = func(ctx context.Context) (*proto.CartMutationReply, error) {
+ return g.client.SetCartItems(ctx, &proto.SetCartItemsRequest{
+ CartId: g.Id.String(),
+ ClientTimestamp: ts,
+ Payload: m,
+ })
+ }
case *proto.OrderCreated:
- env.Mutation = &proto.MutationEnvelope_OrderCompleted{OrderCompleted: m}
+ invoke = func(ctx context.Context) (*proto.CartMutationReply, error) {
+ return g.client.OrderCompleted(ctx, &proto.OrderCompletedRequest{
+ CartId: g.Id.String(),
+ ClientTimestamp: ts,
+ Payload: m,
+ })
+ }
default:
return nil, fmt.Errorf("unsupported mutation type %T", content)
}
+ if invoke == nil {
+ return nil, fmt.Errorf("no invocation mapped for mutation %T", content)
+ }
+
ctx, cancel := context.WithTimeout(context.Background(), g.mutateTimeout)
defer cancel()
- resp, err := g.client.Mutate(ctx, env)
+ resp, err := invoke(ctx)
if err != nil {
return nil, err
}
- // 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)
diff --git a/synced-pool.go b/synced-pool.go
index dfb7494..50535b1 100644
--- a/synced-pool.go
+++ b/synced-pool.go
@@ -426,20 +426,13 @@ func (p *SyncedPool) getGrain(id CartId) (Grain, error) {
return grain, nil
}
-// Process applies mutation(s) to a grain (local or remote).
-func (p *SyncedPool) Process(id CartId, mutations ...interface{}) (*CartGrain, error) {
+// Apply applies a single mutation to a grain (local or remote).
+func (p *SyncedPool) Apply(id CartId, mutation interface{}) (*CartGrain, error) {
grain, err := p.getGrain(id)
if err != nil {
return nil, err
}
- var res *CartGrain
- for _, m := range mutations {
- res, err = grain.Apply(m, false)
- if err != nil {
- return nil, err
- }
- }
- return res, nil
+ return grain.Apply(mutation, false)
}
// Get returns current state of a grain (local or remote).