7.1 KiB
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:
SetUserIdLineItemMarkingSubscriptionAddedPaymentDeclinedConfirmationViewedCreateCheckoutOrder
Each mutation must:
- Define a handler function with signature
func(*CartGrain, *T) error - Be registered in
NewCartMultationRegistry()usingactor.NewMutation - Include unit tests
- Optionally add HTTP/gRPC endpoints if client-invokable
- Update totals if applicable (use
WithTotals())
Mutation Implementations
SetUserId
Purpose: Associates a user ID with the cart for personalization and tracking.
Handler Implementation:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
func ConfirmationViewed(grain *CartGrain, req *messages.ConfirmationViewed) error {
grain.ConfirmationViewCount++
grain.ConfirmationLastViewedAt = time.Now()
return nil
}
Registration:
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:
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:
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:
- Add Handler Function: Implement in
pkg/cart/(e.g.,cart_mutations.go). - Register in Registry: Add to
NewCartMultationRegistry()incart-mutation-helper.go. - Regenerate Proto: Run
protoccommands after any proto changes. - Add Tests: Create unit tests in
pkg/cart/testing the handler logic. - Add Endpoints (if needed): For client-invokable mutations, add HTTP handlers in
cmd/cart/pool-server.go. ConfirmationViewed is handled incmd/cart/checkout_server.gowhen the confirmation page is viewed. - Update Grain Struct: Add any new fields to
CartGraininpkg/cart/grain.go. - 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.