Files
go-cart-actor/pkg/profile/profile-grain.go
T
2026-07-01 10:40:28 +02:00

119 lines
3.8 KiB
Go

// Package profile implements a user-profile actor grain (same event-driven
// framework as the cart, checkout, and order grains). Mutations are the proto
// messages in proto/profile; the grain's event log is the source of truth and
// ProfileGrain is the projection rebuilt by replaying them.
package profile
import (
"encoding/json"
"sync"
"time"
)
// LinkedCart records a cart id linked to this profile.
type LinkedCart struct {
CartId uint64 `json:"cartId"`
Label string `json:"label,omitempty"`
}
// LinkedCheckout records a checkout id linked to this profile.
type LinkedCheckout struct {
CheckoutId uint64 `json:"checkoutId"`
CartId uint64 `json:"cartId"`
}
// LinkedOrder records an order linked to this profile.
type LinkedOrder struct {
OrderReference string `json:"orderReference"`
CartId uint64 `json:"cartId"`
Status string `json:"status,omitempty"`
}
// EmailPreferences controls which transactional emails a customer receives.
// All fields default to true (opted in) when nil/omitted.
type EmailPreferences struct {
// OrderEmails sends order confirmations, shipping updates, receipts.
OrderEmails *bool `json:"orderEmails,omitempty"`
// MarketingEmails sends promotional offers, newsletters, product recommendations.
MarketingEmails *bool `json:"marketingEmails,omitempty"`
}
// StoredAddress is a saved address in the user's address book.
type StoredAddress struct {
Id uint32 `json:"id"`
Label string `json:"label,omitempty"`
FullName string `json:"fullName,omitempty"`
AddressLine1 string `json:"addressLine1"`
AddressLine2 string `json:"addressLine2,omitempty"`
City string `json:"city"`
State string `json:"state,omitempty"`
Zip string `json:"zip"`
Country string `json:"country"`
Phone string `json:"phone,omitempty"`
IsDefaultShipping bool `json:"isDefaultShipping,omitempty"`
IsDefaultBilling bool `json:"isDefaultBilling,omitempty"`
}
// ProfileGrain is the projected current state of a user profile. It implements
// actor.Grain[ProfileGrain].
type ProfileGrain struct {
mu sync.RWMutex
lastAccess time.Time
lastChange time.Time
nextAddrId uint32
Id uint64 `json:"id"`
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
Phone string `json:"phone,omitempty"`
Language string `json:"language,omitempty"`
Currency string `json:"currency,omitempty"`
AvatarUrl string `json:"avatarUrl,omitempty"`
Carts []LinkedCart `json:"carts,omitempty"`
Checkouts []LinkedCheckout `json:"checkouts,omitempty"`
Orders []LinkedOrder `json:"orders,omitempty"`
Addresses []StoredAddress `json:"addresses,omitempty"`
EmailPreferences *EmailPreferences `json:"emailPreferences,omitempty"`
}
// NewProfileGrain returns an empty profile grain for id.
func NewProfileGrain(id uint64, ts time.Time) *ProfileGrain {
return &ProfileGrain{
Id: id,
lastAccess: ts,
lastChange: ts,
}
}
// --- actor.Grain[ProfileGrain] ---
func (p *ProfileGrain) GetId() uint64 { return p.Id }
func (p *ProfileGrain) GetLastAccess() time.Time { return p.lastAccess }
func (p *ProfileGrain) GetLastChange() time.Time { return p.lastChange }
func (p *ProfileGrain) GetCurrentState() (*ProfileGrain, error) {
p.lastAccess = time.Now()
return p, nil
}
func (p *ProfileGrain) GetState() ([]byte, error) { return json.Marshal(p) }
// NextAddrId returns the next address id and advances the counter.
func (p *ProfileGrain) NextAddrId() uint32 {
p.nextAddrId++
return p.nextAddrId
}
// findAddress returns a pointer to the address with the given id, or nil.
func (p *ProfileGrain) findAddress(id uint32) *StoredAddress {
for i := range p.Addresses {
if p.Addresses[i].Id == id {
return &p.Addresses[i]
}
}
return nil
}