238 lines
7.4 KiB
Markdown
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. |