Files
go-cart-actor/NEW_MUTATIONS_SPEC.md
2025-11-20 21:34:39 +01:00

238 lines
7.4 KiB
Markdown

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, and appends a notice with timestamp, message, and optional code for user feedback.
**Handler Implementation**:
```go
func PaymentDeclined(grain *CartGrain, req *messages.PaymentDeclined) error {
grain.PaymentStatus = "declined"
grain.PaymentDeclinedNotices = append(grain.PaymentDeclinedNotices, Notice{
Timestamp: time.Now(),
Message: req.Message,
Code: req.Code,
})
// 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 `PaymentDeclinedNotices` fields. Add status tracking and error notices list to grain. Notice struct has Timestamp, Message, and optional Code.
### 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.