go-cart-actor/NEW_MUTATIONS_SPEC.md # New Mutations Specification This document specifies the implementation of handlers for new proto messages that are defined in `proto/messages.proto` but not yet registered in the mutation registry. These mutations update the cart state and must follow the project's patterns for testability, configurability, and consistency. ## Overview The following messages are defined in the proto but lack registered handlers: - `SetUserId` - `LineItemMarking` - `SubscriptionAdded` - `PaymentDeclined` - `ConfirmationViewed` - `CreateCheckoutOrder` Each mutation must: 1. Define a handler function with signature `func(*CartGrain, *T) error` 2. Be registered in `NewCartMultationRegistry()` using `actor.NewMutation` 3. Include unit tests 4. Optionally add HTTP/gRPC endpoints if client-invokable 5. Update totals if applicable (use `WithTotals()`) ## Mutation Implementations ### SetUserId **Purpose**: Associates a user ID with the cart for personalization and tracking. **Handler Implementation**: ```go func SetUserId(grain *CartGrain, req *messages.SetUserId) error { if req.UserId == "" { return errors.New("user ID cannot be empty") } grain.UserId = req.UserId return nil } ``` **Registration**: ```go actor.NewMutation(SetUserId, func() *messages.SetUserId { return &messages.SetUserId{} }), ``` **Notes**: This is a simple state update. No totals recalculation needed. ### LineItemMarking **Purpose**: Adds or updates a marking (e.g., gift message, special instructions) on a specific line item. **Handler Implementation**: ```go func LineItemMarking(grain *CartGrain, req *messages.LineItemMarking) error { for i, item := range grain.Items { if item.Id == req.Id { grain.Items[i].Marking = &Marking{ Type: req.Type, Text: req.Marking, } return nil } } return fmt.Errorf("item with ID %d not found", req.Id) } ``` **Registration**: ```go actor.NewMutation(LineItemMarking, func() *messages.LineItemMarking { return &messages.LineItemMarking{} }), ``` **Notes**: Assumes `CartGrain.Items` has a `Marking` field (single marking per item). If not, add it to the grain struct. ### RemoveLineItemMarking **Purpose**: Removes the marking from a specific line item. **Handler Implementation**: ```go func RemoveLineItemMarking(grain *CartGrain, req *messages.RemoveLineItemMarking) error { for i, item := range grain.Items { if item.Id == req.Id { grain.Items[i].Marking = nil return nil } } return fmt.Errorf("item with ID %d not found", req.Id) } ``` **Registration**: ```go actor.NewMutation(RemoveLineItemMarking, func() *messages.RemoveLineItemMarking { return &messages.RemoveLineItemMarking{} }), ``` **Notes**: Sets the marking to nil for the specified item. ### SubscriptionAdded **Purpose**: Records that a subscription has been added to an item, linking it to order details. **Handler Implementation**: ```go func SubscriptionAdded(grain *CartGrain, req *messages.SubscriptionAdded) error { for i, item := range grain.Items { if item.Id == req.ItemId { grain.Items[i].SubscriptionDetailsId = req.DetailsId grain.Items[i].OrderReference = req.OrderReference grain.Items[i].IsSubscribed = true return nil } } return fmt.Errorf("item with ID %d not found", req.ItemId) } ``` **Registration**: ```go actor.NewMutation(SubscriptionAdded, func() *messages.SubscriptionAdded { return &messages.SubscriptionAdded{} }), ``` **Notes**: Assumes fields like `SubscriptionDetailsId`, `OrderReference`, `IsSubscribed` exist on items. Add to grain if needed. ### PaymentDeclined **Purpose**: Marks the cart as having a declined payment, potentially updating status or flags. **Handler Implementation**: ```go func PaymentDeclined(grain *CartGrain, req *messages.PaymentDeclined) error { grain.PaymentStatus = "declined" grain.PaymentDeclinedAt = time.Now() // Optionally clear checkout order if in progress if grain.CheckoutOrderId != "" { grain.CheckoutOrderId = "" } return nil } ``` **Registration**: ```go actor.NewMutation(PaymentDeclined, func() *messages.PaymentDeclined { return &messages.PaymentDeclined{} }), ``` **Notes**: Assumes `PaymentStatus` and `PaymentDeclinedAt` fields. Add timestamps and status tracking to grain. ### ConfirmationViewed **Purpose**: Records that the order confirmation has been viewed by the user. Applied automatically when the confirmation page is loaded in checkout_server.go. **Handler Implementation**: ```go func ConfirmationViewed(grain *CartGrain, req *messages.ConfirmationViewed) error { grain.ConfirmationViewCount++ grain.ConfirmationLastViewedAt = time.Now() return nil } ``` **Registration**: ```go actor.NewMutation(ConfirmationViewed, func() *messages.ConfirmationViewed { return &messages.ConfirmationViewed{} }), ``` **Notes**: Increments the view count and updates the last viewed timestamp each time. Assumes `ConfirmationViewCount` and `ConfirmationLastViewedAt` fields. ### CreateCheckoutOrder **Purpose**: Initiates the checkout process, validating terms and creating an order reference. **Handler Implementation**: ```go func CreateCheckoutOrder(grain *CartGrain, req *messages.CreateCheckoutOrder) error { if len(grain.Items) == 0 { return errors.New("cannot checkout empty cart") } if req.Terms != "accepted" { return errors.New("terms must be accepted") } // Validate other fields as needed grain.CheckoutOrderId = generateOrderId() grain.CheckoutStatus = "pending" grain.CheckoutCountry = req.Country return nil } ``` **Registration**: ```go actor.NewMutation(CreateCheckoutOrder, func() *messages.CreateCheckoutOrder { return &messages.CreateCheckoutOrder{} }).WithTotals(), ``` **Notes**: Use `WithTotals()` to recalculate totals after checkout initiation. Assumes order ID generation function. ## Implementation Steps For each mutation: 1. **Add Handler Function**: Implement in `pkg/cart/` (e.g., `cart_mutations.go`). 2. **Register in Registry**: Add to `NewCartMultationRegistry()` in `cart-mutation-helper.go`. 3. **Regenerate Proto**: Run `protoc` commands after any proto changes. 4. **Add Tests**: Create unit tests in `pkg/cart/` testing the handler logic. 5. **Add Endpoints** (if needed): For client-invokable mutations, add HTTP handlers in `cmd/cart/pool-server.go`. ConfirmationViewed is handled in `cmd/cart/checkout_server.go` when the confirmation page is viewed. 6. **Update Grain Struct**: Add any new fields to `CartGrain` in `pkg/cart/grain.go`. 7. **Run Tests**: Ensure `go test ./...` passes. ## Testing Guidelines - Mock dependencies using interfaces. - Test error cases (e.g., invalid IDs, empty carts). - Verify state changes and totals recalculation. - Use table-driven tests for multiple scenarios. ## Configuration and Testability Follow `MutationRegistry` and `SimpleGrainPool` patterns: - Use interfaces for external dependencies (e.g., ID generators). - Inject configurations via constructor parameters. - Avoid global state; make handlers pure functions where possible.