split cart and checkout and checkout vs payments
This commit is contained in:
184
pkg/checkout/checkout-grain.go
Normal file
184
pkg/checkout/checkout-grain.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package checkout
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||
)
|
||||
|
||||
// CheckoutId is the same as CartId for simplicity
|
||||
type CheckoutId = cart.CartId
|
||||
|
||||
type PickupPoint struct {
|
||||
DeliveryId uint32 `json:"deliveryId,omitempty"`
|
||||
Id string `json:"id"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Address *string `json:"address,omitempty"`
|
||||
City *string `json:"city,omitempty"`
|
||||
Zip *string `json:"zip,omitempty"`
|
||||
Country *string `json:"country,omitempty"`
|
||||
}
|
||||
|
||||
type CheckoutDelivery struct {
|
||||
Id uint32 `json:"id"`
|
||||
Provider string `json:"provider"`
|
||||
Price cart.Price `json:"price"`
|
||||
Items []uint32 `json:"items"`
|
||||
PickupPoint *PickupPoint `json:"pickupPoint,omitempty"`
|
||||
}
|
||||
type PaymentStatus string
|
||||
type CheckoutPaymentStatus PaymentStatus
|
||||
|
||||
const (
|
||||
PaymentStatusPending PaymentStatus = "pending"
|
||||
PaymentStatusFailed PaymentStatus = "failed"
|
||||
PaymentStatusSuccess PaymentStatus = "success"
|
||||
CheckoutPaymentStatusPending CheckoutPaymentStatus = "pending"
|
||||
CheckoutPaymentStatusFailed CheckoutPaymentStatus = "failed"
|
||||
CheckoutPaymentStatusSuccess CheckoutPaymentStatus = "success"
|
||||
CheckoutPaymentStatusCancelled CheckoutPaymentStatus = "partial"
|
||||
)
|
||||
|
||||
type Payment struct {
|
||||
PaymentId string `json:"paymentId"`
|
||||
Status PaymentStatus `json:"status"`
|
||||
Amount int64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Method *string `json:"method,omitempty"`
|
||||
Events []*PaymentEvent `json:"events,omitempty"`
|
||||
ProcessorReference *string `json:"processorReference,omitempty"`
|
||||
StartedAt *time.Time `json:"startedAt,omitempty"`
|
||||
CompletedAt *time.Time `json:"completedAt,omitempty"`
|
||||
}
|
||||
|
||||
type PaymentEvent struct {
|
||||
Name string `json:"name"`
|
||||
Success bool `json:"success"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
func (p *Payment) IsSettled() bool {
|
||||
if p == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch p.Status {
|
||||
case PaymentStatusSuccess:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type ConfirmationStatus struct {
|
||||
Code *string `json:"code,omitempty"`
|
||||
ViewCount int `json:"viewCount"`
|
||||
LastViewedAt time.Time `json:"lastViewedAt"`
|
||||
}
|
||||
|
||||
type CheckoutGrain struct {
|
||||
mu sync.RWMutex
|
||||
lastDeliveryId uint32
|
||||
lastGiftcardId uint32
|
||||
lastAccess time.Time
|
||||
lastChange time.Time
|
||||
Version uint32
|
||||
Id CheckoutId `json:"id"`
|
||||
CartId cart.CartId `json:"cartId"`
|
||||
CartVersion uint64 `json:"cartVersion"`
|
||||
CartState *cart.CartGrain `json:"cartState"` // snapshot of items
|
||||
CartTotalPrice *cart.Price `json:"cartTotalPrice"`
|
||||
OrderId *string `json:"orderId"`
|
||||
Deliveries []*CheckoutDelivery `json:"deliveries,omitempty"`
|
||||
PaymentInProgress uint16 `json:"paymentInProgress"`
|
||||
InventoryReserved bool `json:"inventoryReserved"`
|
||||
Confirmation *ConfirmationStatus `json:"confirmationViewed,omitempty"`
|
||||
Payments []*Payment `json:"payments,omitempty"`
|
||||
}
|
||||
|
||||
func NewCheckoutGrain(id uint64, cartId cart.CartId, cartVersion uint64, ts time.Time, cartState *cart.CartGrain) *CheckoutGrain {
|
||||
return &CheckoutGrain{
|
||||
lastDeliveryId: 0,
|
||||
lastGiftcardId: 0,
|
||||
lastAccess: ts,
|
||||
lastChange: ts,
|
||||
Id: CheckoutId(id),
|
||||
CartId: cartId,
|
||||
CartVersion: cartVersion,
|
||||
Deliveries: []*CheckoutDelivery{},
|
||||
Payments: []*Payment{},
|
||||
CartState: cartState,
|
||||
CartTotalPrice: cartState.TotalPrice,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CheckoutGrain) GetId() uint64 {
|
||||
return uint64(c.Id)
|
||||
}
|
||||
|
||||
func (c *CheckoutGrain) GetLastChange() time.Time {
|
||||
return c.lastChange
|
||||
}
|
||||
|
||||
func (c *CheckoutGrain) GetLastAccess() time.Time {
|
||||
return c.lastAccess
|
||||
}
|
||||
|
||||
func (c *CheckoutGrain) GetCurrentState() (*CheckoutGrain, error) {
|
||||
c.lastAccess = time.Now()
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *CheckoutGrain) GetState() ([]byte, error) {
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
func (c *CheckoutGrain) FindPayment(paymentId string) (*Payment, bool) {
|
||||
if paymentId == "" {
|
||||
return nil, false
|
||||
}
|
||||
for _, payment := range c.Payments {
|
||||
if payment != nil && payment.PaymentId == paymentId {
|
||||
return payment, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (c *CheckoutGrain) SettledPayments() []*Payment {
|
||||
if len(c.Payments) == 0 {
|
||||
return nil
|
||||
}
|
||||
settled := make([]*Payment, 0, len(c.Payments))
|
||||
for _, payment := range c.Payments {
|
||||
if payment != nil && payment.IsSettled() {
|
||||
settled = append(settled, payment)
|
||||
}
|
||||
}
|
||||
if len(settled) == 0 {
|
||||
return nil
|
||||
}
|
||||
return settled
|
||||
}
|
||||
|
||||
func (c *CheckoutGrain) OpenPayments() []*Payment {
|
||||
if len(c.Payments) == 0 {
|
||||
return nil
|
||||
}
|
||||
pending := make([]*Payment, 0, len(c.Payments))
|
||||
for _, payment := range c.Payments {
|
||||
if payment == nil {
|
||||
continue
|
||||
}
|
||||
if !payment.IsSettled() {
|
||||
pending = append(pending, payment)
|
||||
}
|
||||
}
|
||||
if len(pending) == 0 {
|
||||
return nil
|
||||
}
|
||||
return pending
|
||||
}
|
||||
35
pkg/checkout/mutation-context.go
Normal file
35
pkg/checkout/mutation-context.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package checkout
|
||||
|
||||
import (
|
||||
"git.k6n.net/go-cart-actor/pkg/actor"
|
||||
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||
)
|
||||
|
||||
type CheckoutMutationContext struct {
|
||||
// Add any services needed, e.g., for delivery calculations, but since inventory is pre-handled, maybe none
|
||||
}
|
||||
|
||||
func NewCheckoutMutationContext() *CheckoutMutationContext {
|
||||
return &CheckoutMutationContext{}
|
||||
}
|
||||
|
||||
func NewCheckoutMutationRegistry(ctx *CheckoutMutationContext) actor.MutationRegistry {
|
||||
reg := actor.NewMutationRegistry()
|
||||
reg.RegisterMutations(
|
||||
actor.NewMutation(HandleInitializeCheckout, func() *messages.InitializeCheckout { return &messages.InitializeCheckout{} }),
|
||||
actor.NewMutation(HandlePaymentStarted, func() *messages.PaymentStarted { return &messages.PaymentStarted{} }),
|
||||
actor.NewMutation(HandlePaymentCompleted, func() *messages.PaymentCompleted { return &messages.PaymentCompleted{} }),
|
||||
actor.NewMutation(HandlePaymentDeclined, func() *messages.PaymentDeclined { return &messages.PaymentDeclined{} }),
|
||||
actor.NewMutation(HandlePaymentEvent, func() *messages.PaymentEvent { return &messages.PaymentEvent{} }),
|
||||
actor.NewMutation(HandleConfirmationViewed, func() *messages.ConfirmationViewed { return &messages.ConfirmationViewed{} }),
|
||||
//actor.NewMutation(HandleCreateCheckoutOrder, func() *messages.CreateCheckoutOrder { return &messages.CreateCheckoutOrder{} }),
|
||||
actor.NewMutation(HandleOrderCreated, func() *messages.OrderCreated { return &messages.OrderCreated{} }),
|
||||
actor.NewMutation(HandleInventoryReserved, func() *messages.InventoryReserved { return &messages.InventoryReserved{} }),
|
||||
actor.NewMutation(HandleSetDelivery, func() *messages.SetDelivery { return &messages.SetDelivery{} }),
|
||||
actor.NewMutation(HandleSetPickupPoint, func() *messages.SetPickupPoint { return &messages.SetPickupPoint{} }),
|
||||
actor.NewMutation(HandleRemoveDelivery, func() *messages.RemoveDelivery { return &messages.RemoveDelivery{} }),
|
||||
// actor.NewMutation(HandleAddGiftcard, func() *messages.AddGiftcard { return &messages.AddGiftcard{} }),
|
||||
// actor.NewMutation(HandleRemoveGiftcard, func() *messages.RemoveGiftcard { return &messages.RemoveGiftcard{} }),
|
||||
)
|
||||
return reg
|
||||
}
|
||||
26
pkg/checkout/mutation_confirmation_viewed.go
Normal file
26
pkg/checkout/mutation_confirmation_viewed.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package checkout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||
)
|
||||
|
||||
func HandleConfirmationViewed(g *CheckoutGrain, m *messages.ConfirmationViewed) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("ConfirmationViewed: nil payload")
|
||||
}
|
||||
|
||||
if g.Confirmation != nil {
|
||||
g.Confirmation = &ConfirmationStatus{
|
||||
ViewCount: 1,
|
||||
LastViewedAt: time.Now(),
|
||||
}
|
||||
} else {
|
||||
g.Confirmation.ViewCount++
|
||||
g.Confirmation.LastViewedAt = time.Now()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
55
pkg/checkout/mutation_delivery.go
Normal file
55
pkg/checkout/mutation_delivery.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package checkout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.k6n.net/go-cart-actor/pkg/cart"
|
||||
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||
)
|
||||
|
||||
func asPickupPoint(p *messages.PickupPoint, deliveryId uint32) *PickupPoint {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return &PickupPoint{
|
||||
Id: p.Id,
|
||||
Name: p.Name,
|
||||
Address: p.Address,
|
||||
City: p.City,
|
||||
Country: p.Country,
|
||||
Zip: p.Zip,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleSetDelivery mutation
|
||||
// HandleSetDelivery mutation
|
||||
func HandleSetDelivery(g *CheckoutGrain, m *messages.SetDelivery) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("HandleSetDelivery: nil payload")
|
||||
}
|
||||
if m.Provider == "" {
|
||||
return fmt.Errorf("HandleSetDelivery: missing provider")
|
||||
}
|
||||
|
||||
// Check if delivery already exists, update or add
|
||||
for _, d := range g.Deliveries {
|
||||
if d.Provider == m.Provider {
|
||||
// Update existing
|
||||
d.Items = m.Items
|
||||
d.PickupPoint = asPickupPoint(m.PickupPoint, d.Id)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Add new delivery
|
||||
g.lastDeliveryId++
|
||||
delivery := &CheckoutDelivery{
|
||||
Id: g.lastDeliveryId,
|
||||
Provider: m.Provider,
|
||||
Items: m.Items,
|
||||
PickupPoint: asPickupPoint(m.PickupPoint, g.lastDeliveryId),
|
||||
Price: *cart.NewPrice(), // Price might need calculation, but for now zero
|
||||
}
|
||||
g.Deliveries = append(g.Deliveries, delivery)
|
||||
return nil
|
||||
}
|
||||
44
pkg/checkout/mutation_initialize_checkout.go
Normal file
44
pkg/checkout/mutation_initialize_checkout.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package checkout
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||
)
|
||||
|
||||
// mutation_initialize_checkout.go
|
||||
//
|
||||
// Registers the InitializeCheckout mutation.
|
||||
// This mutation is invoked AFTER an external checkout session
|
||||
// has been successfully created or updated. It persists the
|
||||
// order reference / status and marks the checkout as having a payment in progress.
|
||||
//
|
||||
// Behavior:
|
||||
// - Sets OrderId to the order ID.
|
||||
// - Sets Status to the current status.
|
||||
// - Sets PaymentInProgress flag.
|
||||
// - Assumes inventory is already reserved.
|
||||
//
|
||||
// Validation:
|
||||
// - Returns an error if payload is nil.
|
||||
// - Returns an error if orderId is empty.
|
||||
|
||||
func HandleInitializeCheckout(g *CheckoutGrain, m *messages.InitializeCheckout) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("InitializeCheckout: nil payload")
|
||||
}
|
||||
if m.OrderId == "" {
|
||||
return fmt.Errorf("InitializeCheckout: missing orderId")
|
||||
}
|
||||
if m.CartState != nil {
|
||||
return fmt.Errorf("InitializeCheckout: checkout already initialized")
|
||||
}
|
||||
|
||||
err := json.Unmarshal(m.CartState.Value, &g.CartState)
|
||||
if err != nil {
|
||||
return fmt.Errorf("InitializeCheckout: failed to unmarshal cart state: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
16
pkg/checkout/mutation_inventory_reserved.go
Normal file
16
pkg/checkout/mutation_inventory_reserved.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package checkout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||
)
|
||||
|
||||
func HandleInventoryReserved(g *CheckoutGrain, m *messages.InventoryReserved) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("HandleInventoryReserved: nil payload")
|
||||
}
|
||||
|
||||
g.InventoryReserved = m.Status == "success"
|
||||
return nil
|
||||
}
|
||||
49
pkg/checkout/mutation_order_created.go
Normal file
49
pkg/checkout/mutation_order_created.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package checkout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||
)
|
||||
|
||||
// mutation_order_created.go
|
||||
//
|
||||
// Registers the OrderCreated mutation.
|
||||
//
|
||||
// This mutation represents the completion (or state transition) of an order
|
||||
// initiated earlier via InitializeCheckout / external processing.
|
||||
// It finalizes (or updates) the checkout's order metadata.
|
||||
//
|
||||
// Behavior:
|
||||
// - Validates payload non-nil and OrderId not empty.
|
||||
// - Sets Status from payload.Status.
|
||||
// - Sets OrderId if not already set.
|
||||
// - Does NOT adjust monetary totals.
|
||||
//
|
||||
// Notes / Future Extensions:
|
||||
// - If multiple order completion events can arrive (e.g., retries / webhook
|
||||
// replays), this handler is idempotent: it simply overwrites fields.
|
||||
// - If you need to guard against conflicting order IDs, add a check:
|
||||
// if g.OrderId != "" && g.OrderId != m.OrderId { ... }
|
||||
// - Add audit logging or metrics here if required.
|
||||
//
|
||||
// Concurrency:
|
||||
// - Relies on the higher-level guarantee that Apply() calls are serialized
|
||||
// per grain. If out-of-order events are possible, embed versioning or
|
||||
// timestamps in the mutation and compare before applying changes.
|
||||
|
||||
func HandleOrderCreated(g *CheckoutGrain, m *messages.OrderCreated) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("HandleOrderCreated: nil payload")
|
||||
}
|
||||
if m.OrderId == "" {
|
||||
return fmt.Errorf("OrderCreated: missing orderId")
|
||||
}
|
||||
if g.OrderId == nil {
|
||||
g.OrderId = &m.OrderId
|
||||
} else if *g.OrderId != m.OrderId {
|
||||
return fmt.Errorf("OrderCreated: conflicting order ID")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
37
pkg/checkout/mutation_payment_completed.go
Normal file
37
pkg/checkout/mutation_payment_completed.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package checkout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||
)
|
||||
|
||||
// PaymentCompleted registers the completion of a payment for a checkout.
|
||||
func HandlePaymentCompleted(g *CheckoutGrain, m *messages.PaymentCompleted) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("PaymentCompleted: nil payload")
|
||||
}
|
||||
paymentId := m.PaymentId
|
||||
payment, found := g.FindPayment(paymentId)
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("PaymentCompleted: payment not found")
|
||||
}
|
||||
|
||||
payment.ProcessorReference = m.ProcessorReference
|
||||
payment.Status = PaymentStatusSuccess
|
||||
payment.Amount = m.Amount
|
||||
payment.Currency = m.Currency
|
||||
payment.CompletedAt = &time.Time{}
|
||||
if m.CompletedAt != nil {
|
||||
*payment.CompletedAt = m.CompletedAt.AsTime()
|
||||
} else {
|
||||
*payment.CompletedAt = time.Now()
|
||||
}
|
||||
|
||||
// Update checkout status
|
||||
g.PaymentInProgress--
|
||||
|
||||
return nil
|
||||
}
|
||||
28
pkg/checkout/mutation_payment_declined.go
Normal file
28
pkg/checkout/mutation_payment_declined.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package checkout
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||
)
|
||||
|
||||
func asPointer[T any](value T) *T {
|
||||
return &value
|
||||
}
|
||||
|
||||
var ErrPaymentNotFound = errors.New("payment not found")
|
||||
|
||||
func HandlePaymentDeclined(g *CheckoutGrain, m *messages.PaymentDeclined) error {
|
||||
|
||||
payment, found := g.FindPayment(m.PaymentId)
|
||||
if !found {
|
||||
return ErrPaymentNotFound
|
||||
}
|
||||
|
||||
payment.CompletedAt = asPointer(time.Now())
|
||||
payment.Status = "failed"
|
||||
g.PaymentInProgress--
|
||||
|
||||
return nil
|
||||
}
|
||||
20
pkg/checkout/mutation_payment_event.go
Normal file
20
pkg/checkout/mutation_payment_event.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package checkout
|
||||
|
||||
import (
|
||||
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||
)
|
||||
|
||||
func HandlePaymentEvent(g *CheckoutGrain, m *messages.PaymentEvent) error {
|
||||
|
||||
payment, found := g.FindPayment(m.PaymentId)
|
||||
if !found {
|
||||
return ErrPaymentNotFound
|
||||
}
|
||||
metaBytes := m.Data.Value
|
||||
payment.Events = append(payment.Events, &PaymentEvent{
|
||||
Name: m.Name,
|
||||
Success: m.Success,
|
||||
Data: metaBytes,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
84
pkg/checkout/mutation_payment_started.go
Normal file
84
pkg/checkout/mutation_payment_started.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package checkout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||
)
|
||||
|
||||
// PaymentStarted registers the beginning of a payment attempt for a checkout.
|
||||
// It either upserts the payment entry (based on paymentId) or creates a new one,
|
||||
// marks the checkout as having a payment in progress.
|
||||
func HandlePaymentStarted(g *CheckoutGrain, m *messages.PaymentStarted) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("PaymentStarted: nil payload")
|
||||
}
|
||||
paymentID := strings.TrimSpace(m.PaymentId)
|
||||
if paymentID == "" {
|
||||
return fmt.Errorf("PaymentStarted: missing paymentId")
|
||||
}
|
||||
if m.Amount < 0 {
|
||||
return fmt.Errorf("PaymentStarted: amount cannot be negative")
|
||||
}
|
||||
|
||||
currency := strings.TrimSpace(m.Currency)
|
||||
provider := strings.TrimSpace(m.Provider)
|
||||
method := copyOptionalString(m.Method)
|
||||
|
||||
startedAt := time.Now().UTC()
|
||||
if m.StartedAt != nil {
|
||||
startedAt = m.StartedAt.AsTime()
|
||||
}
|
||||
|
||||
payment, found := g.FindPayment(paymentID)
|
||||
|
||||
if found {
|
||||
if payment.Status != "pending" {
|
||||
return fmt.Errorf("PaymentStarted: payment already started")
|
||||
}
|
||||
if payment.PaymentId != paymentID {
|
||||
payment.PaymentId = paymentID
|
||||
}
|
||||
payment.Status = "pending"
|
||||
payment.Amount = m.Amount
|
||||
if currency != "" {
|
||||
payment.Currency = currency
|
||||
}
|
||||
if provider != "" {
|
||||
payment.Provider = provider
|
||||
}
|
||||
if method != nil {
|
||||
payment.Method = method
|
||||
}
|
||||
payment.StartedAt = &startedAt
|
||||
payment.CompletedAt = nil
|
||||
payment.ProcessorReference = nil
|
||||
} else {
|
||||
g.PaymentInProgress++
|
||||
g.Payments = append(g.Payments, &Payment{
|
||||
PaymentId: paymentID,
|
||||
Status: "pending",
|
||||
Amount: m.Amount,
|
||||
Currency: currency,
|
||||
Provider: provider,
|
||||
Method: method,
|
||||
StartedAt: &startedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyOptionalString(src *string) *string {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
trimmed := strings.TrimSpace(*src)
|
||||
if trimmed == "" {
|
||||
return nil
|
||||
}
|
||||
dst := trimmed
|
||||
return &dst
|
||||
}
|
||||
3
pkg/checkout/mutation_payments.go
Normal file
3
pkg/checkout/mutation_payments.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package checkout
|
||||
|
||||
// This file is now empty as mutations have been moved to separate files.
|
||||
38
pkg/checkout/mutation_remove_delivery.go
Normal file
38
pkg/checkout/mutation_remove_delivery.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package checkout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||
)
|
||||
|
||||
// mutation_remove_delivery.go
|
||||
//
|
||||
// Registers the RemoveDelivery mutation.
|
||||
//
|
||||
// Behavior:
|
||||
// - Removes the delivery entry whose Id == payload.Id.
|
||||
// - If not found, returns an error.
|
||||
// - Items previously associated with that delivery simply become "without delivery";
|
||||
// subsequent delivery mutations can reassign them.
|
||||
|
||||
func HandleRemoveDelivery(g *CheckoutGrain, m *messages.RemoveDelivery) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("RemoveDelivery: nil payload")
|
||||
}
|
||||
targetID := uint32(m.Id)
|
||||
index := -1
|
||||
for i, d := range g.Deliveries {
|
||||
if d.Id == targetID {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
return fmt.Errorf("RemoveDelivery: delivery id %d not found", m.Id)
|
||||
}
|
||||
|
||||
// Remove delivery (order not preserved beyond necessity)
|
||||
g.Deliveries = append(g.Deliveries[:index], g.Deliveries[index+1:]...)
|
||||
return nil
|
||||
}
|
||||
51
pkg/checkout/mutation_set_pickup_point.go
Normal file
51
pkg/checkout/mutation_set_pickup_point.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package checkout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
messages "git.k6n.net/go-cart-actor/proto/checkout"
|
||||
)
|
||||
|
||||
// mutation_set_pickup_point.go
|
||||
//
|
||||
// Registers the SetPickupPoint mutation using the generic mutation registry.
|
||||
//
|
||||
// Semantics (mirrors original switch-based implementation):
|
||||
// - Locate the delivery with Id == payload.DeliveryId
|
||||
// - Set (or overwrite) its PickupPoint with the provided data
|
||||
// - Does NOT alter pricing or taxes (so no totals recalculation required)
|
||||
//
|
||||
// Validation / Error Handling:
|
||||
// - If payload is nil -> error
|
||||
// - If DeliveryId not found -> error
|
||||
//
|
||||
// Concurrency:
|
||||
// - Relies on the existing expectation that higher-level mutation routing
|
||||
// serializes Apply() calls per grain; if stricter guarantees are needed,
|
||||
// a delivery-level lock could be introduced later.
|
||||
//
|
||||
// Future Extensions:
|
||||
// - Validate pickup point fields (country code, zip format, etc.)
|
||||
// - Track history / audit of pickup point changes
|
||||
// - Trigger delivery price adjustments (which would then require WithTotals()).
|
||||
|
||||
func HandleSetPickupPoint(g *CheckoutGrain, m *messages.SetPickupPoint) error {
|
||||
if m == nil {
|
||||
return fmt.Errorf("SetPickupPoint: nil payload")
|
||||
}
|
||||
|
||||
for _, d := range g.Deliveries {
|
||||
if d.Id == uint32(m.DeliveryId) {
|
||||
d.PickupPoint = &PickupPoint{
|
||||
Id: m.PickupPoint.Id,
|
||||
Name: m.PickupPoint.Name,
|
||||
Address: m.PickupPoint.Address,
|
||||
City: m.PickupPoint.City,
|
||||
Zip: m.PickupPoint.Zip,
|
||||
Country: m.PickupPoint.Country,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("SetPickupPoint: delivery id %d not found", m.DeliveryId)
|
||||
}
|
||||
Reference in New Issue
Block a user