Files
go-cart-actor/NEW_MUTATIONS_SPEC.md
matst80 60cd6cfd51
All checks were successful
Build and Publish / BuildAndDeployAmd64 (push) Successful in 44s
Build and Publish / BuildAndDeployArm64 (push) Successful in 5m3s
update
2025-11-20 21:20:35 +01:00

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:

  • 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:

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:

  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.