Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide coffee details #7

Merged
merged 2 commits into from
Feb 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions internal/api/coffee.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func CreateCoffee(service *product.Service) func(*gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
b, err := service.Create(json.Name, json.Price, json.CuppingScore)
b, err := service.Create(json.Name, json.Price, json.CuppingScore, &json.Details)
if err != nil {
log.Println(err)
switch {
Expand All @@ -85,16 +85,18 @@ func CreateCoffee(service *product.Service) func(*gin.Context) {
}

type CreateCoffeeRequest struct {
Name string `form:"name" json:"name" binding:"required"`
Price float64 `form:"price" json:"price" binding:"required"`
CuppingScore *int `form:"cupping_score" json:"cupping_score,omitempty"`
Name string `form:"name" json:"name" binding:"required"`
Price float64 `form:"price" json:"price" binding:"required"`
CuppingScore *int `form:"cupping_score" json:"cupping_score,omitempty"`
Details product.CoffeeDetails `form:"info" json:"info"`
}

type CoffeeInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
CuppingScore int `json:"cupping_score"`
ID string `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
CuppingScore int `json:"cupping_score"`
Details product.CoffeeDetails `json:"info"`
}

func allToCoffeeInfo(list []product.Coffee) ([]CoffeeInfo, error) {
Expand All @@ -116,5 +118,10 @@ func toCoffeeInfo(b *product.Coffee) (CoffeeInfo, error) {
if b == nil {
return CoffeeInfo{}, errors.New("beverage is nil")
}
return CoffeeInfo{ID: b.AggregateID, Name: b.Type, Price: b.Price(), CuppingScore: b.CoffeeValue().Value}, nil
d := toCoffeeDetails(b.Details())
return CoffeeInfo{ID: b.AggregateID, Name: b.Type, Price: b.Price(), CuppingScore: b.CoffeeValue().Value, Details: d}, nil
}

func toCoffeeDetails(d product.Details) product.CoffeeDetails {
return product.CoffeeDetails{Origin: d.Origin, Description: d.Description, Misc: d.Misc}
}
64 changes: 59 additions & 5 deletions internal/product/coffee.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import (

// Coffee is the actual representation of the black gold.
type Coffee struct {
AggregateID string
Type string
price float64
cva CuppingScore
events []event.Event
AggregateID string // The ID to identify the coffee in the system
Type string // What type of coffee it is, e.g. black coffee, espresso, ...
price float64 // The current price of the coffee in €, e.g. 0.50 for 50 cents
cva CuppingScore // The coffee value assessment result, currently the CuppingScore
details Details // A more detailed description about the coffee
events []event.Event // Uncommitted events of the aggregate
}

// CoffeeValue provides the assessed Value of the current coffee.
Expand Down Expand Up @@ -89,6 +90,49 @@ func (e CvaProvided) Occurred() time.Time {
return e.OccurredOn
}

// Details enables a containerised description with more detail of a Coffee.
type Details struct {
Origin string `json:"origin"` // The country the coffee has been produced
Description string `json:"description"` // Some detailed description about the coffee
RoastHouse string `json:"roast_house"` // The location the coffee has been roasted
Misc map[string]string `json:"misc"` // An unstructured collection of key:values to provide more details
}

func (c *Coffee) Details() Details {
return c.details
}

// UpdateDetails sets some more detailed information for the current Coffee.
func (c *Coffee) UpdateDetails(details Details) error {
e := NewDetailsUpdated(c.AggregateID, details)
if err := c.apply(e); err != nil {
return errors.Join(fmt.Errorf("could not update details for %s [id: %s]", c.Type, c.AggregateID), err)
}
return nil
}

type DetailsUpdated struct {
ID string
Details Details
OccurredOn time.Time
}

func (e DetailsUpdated) AggregateID() string {
return e.ID
}

func (e DetailsUpdated) Type() string {
return "DetailsUpdated"
}

func (e DetailsUpdated) Occurred() time.Time {
return e.OccurredOn
}

func NewDetailsUpdated(id string, details Details) DetailsUpdated {
return DetailsUpdated{ID: id, Details: details, OccurredOn: time.Now()}
}

// ChangePrice updates the price of the current Coffee.
//
// Only values greater or equal zero are allowed.
Expand Down Expand Up @@ -135,6 +179,10 @@ func (c *Coffee) apply(e event.Event) error {
if err := c.applyCva(theEvent); err != nil {
return err
}
case DetailsUpdated:
if err := c.applyDetails(theEvent); err != nil {
return err
}
default:
return fmt.Errorf("cannot apply event: unknown event '%T'", e)
}
Expand Down Expand Up @@ -170,6 +218,12 @@ func (c *Coffee) applyCva(e CvaProvided) error {
return nil
}

func (c *Coffee) applyDetails(e DetailsUpdated) error {
c.details = e.Details
c.events = append(c.events, e)
return nil
}

type CoffeeCreated struct {
ID string
BeverageType string
Expand Down
25 changes: 25 additions & 0 deletions internal/product/coffee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,28 @@ func TestCVA(t *testing.T) {
return
}
}

func TestDetails(t *testing.T) {
c, _ := NewCoffee("Black Coffee", 0.25)
c.Clear()

d := Details{"Kirinyaga County", "Kenianischer Waldkaffee", "", nil}

err := c.UpdateDetails(d)

if err != nil {
t.Errorf("UpdateDetails() error = %v", err)
}

if !(c.Details().Origin == "Kirinyaga County") {
t.Errorf("Details should have been Kirinyaga County")
}

switch c.Events()[0].(type) {
case DetailsUpdated:
break
default:
t.Errorf("Events() should have returned a DetailsUpdated, but returned %v", c.Events()[0])
}

}
42 changes: 41 additions & 1 deletion internal/product/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,29 @@ func convert(entry storage.EventEntry) (event.Event, error) {
return nil, err
}
return evnt, nil
case "DetailsUpdated":
evnt, err := toDetailsUpdated(entry)
if err != nil {
return nil, err
}
return evnt, nil
default:
return nil, fmt.Errorf("unknown event type '%s'", entry.EventType)
}
}

func toDetailsUpdated(entry storage.EventEntry) (DetailsUpdated, error) {
e := DetailsUpdated{}
err := json.Unmarshal(entry.EventData, &e)
if err != nil {
return e, fmt.Errorf("failed to unmarshal expected DetailsUpdated event data: %w", err)
}
return e, nil
}

var InvalidPropertyError = errors.New("invalid property")

func (s *Service) Create(name string, price float64, score *int) (*Coffee, error) {
func (s *Service) Create(name string, price float64, score *int, details *CoffeeDetails) (*Coffee, error) {
b, err := NewCoffee(name, price)
if err != nil {
return nil, fmt.Errorf("%w: %s", InvalidPropertyError, err.Error())
Expand All @@ -104,6 +119,18 @@ func (s *Service) Create(name string, price float64, score *int) (*Coffee, error
return nil, fmt.Errorf("%w: %s", InvalidPropertyError, err.Error())
}

// Details are optional
if details != nil {
d := Details{
Origin: details.Origin,
Description: details.Description,
RoastHouse: details.RoastHouse,
Misc: details.Misc}
if err = b.UpdateDetails(d); err != nil {
return nil, fmt.Errorf("%w: %s", InvalidPropertyError, err.Error())
}
}

entries := make([]storage.EventEntry, 0)
for _, e := range b.Events() {
entry, err := toEventEntry(e)
Expand Down Expand Up @@ -139,6 +166,12 @@ func toEventEntry(event event.Event) (storage.EventEntry, error) {
return storage.EventEntry{}, err
}
return storage.EventEntry{AggregateID: t.AggregateID(), Date: t.OccurredOn, EventType: "CvaProvided", EventData: data}, nil
case DetailsUpdated:
data, err := json.Marshal(t)
if err != nil {
return storage.EventEntry{}, err
}
return storage.EventEntry{AggregateID: t.AggregateID(), Date: t.OccurredOn, EventType: "DetailsUpdated", EventData: data}, nil
default:
return storage.EventEntry{}, fmt.Errorf("unknown event type: %T", t)
}
Expand Down Expand Up @@ -167,3 +200,10 @@ func toCvaProvided(entry storage.EventEntry) (event.Event, error) {
}
return e, nil
}

type CoffeeDetails struct {
Origin string `json:"origin"`
Description string `json:"description"`
RoastHouse string `json:"roast_house"`
Misc map[string]string `json:"misc"`
}
Loading