From df68dfcf38790fe03d3ecf57e897ce7d17ea7980 Mon Sep 17 00:00:00 2001 From: Sven Fillinger Date: Sun, 9 Feb 2025 09:58:51 +0100 Subject: [PATCH 1/2] Enable coffee details --- internal/api/coffee.go | 25 ++++++++----- internal/product/coffee.go | 64 ++++++++++++++++++++++++++++++--- internal/product/coffee_test.go | 25 +++++++++++++ internal/product/service.go | 42 +++++++++++++++++++++- 4 files changed, 141 insertions(+), 15 deletions(-) diff --git a/internal/api/coffee.go b/internal/api/coffee.go index 9c97125..afdee1a 100644 --- a/internal/api/coffee.go +++ b/internal/api/coffee.go @@ -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 { @@ -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"` + CoffeeDetails product.CoffeeDetails `json:"info"` } func allToCoffeeInfo(list []product.Coffee) ([]CoffeeInfo, error) { @@ -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, CoffeeDetails: d}, nil +} + +func toCoffeeDetails(d product.Details) product.CoffeeDetails { + return product.CoffeeDetails{Origin: d.Origin, Description: d.Description, Misc: d.Misc} } diff --git a/internal/product/coffee.go b/internal/product/coffee.go index 4fa2b6f..1de5ad4 100644 --- a/internal/product/coffee.go +++ b/internal/product/coffee.go @@ -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. @@ -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. @@ -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) } @@ -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 diff --git a/internal/product/coffee_test.go b/internal/product/coffee_test.go index fc25af3..1fc744e 100644 --- a/internal/product/coffee_test.go +++ b/internal/product/coffee_test.go @@ -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]) + } + +} diff --git a/internal/product/service.go b/internal/product/service.go index d797e93..c5bc1fa 100644 --- a/internal/product/service.go +++ b/internal/product/service.go @@ -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()) @@ -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) @@ -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) } @@ -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"` +} From ed46ab6acc2e096f58e221815713a9a75a7626bf Mon Sep 17 00:00:00 2001 From: Sven Fillinger Date: Sun, 9 Feb 2025 10:01:57 +0100 Subject: [PATCH 2/2] Shorten property name --- internal/api/coffee.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/api/coffee.go b/internal/api/coffee.go index afdee1a..f93c9e0 100644 --- a/internal/api/coffee.go +++ b/internal/api/coffee.go @@ -92,11 +92,11 @@ type CreateCoffeeRequest struct { } type CoffeeInfo struct { - ID string `json:"id"` - Name string `json:"name"` - Price float64 `json:"price"` - CuppingScore int `json:"cupping_score"` - CoffeeDetails product.CoffeeDetails `json:"info"` + 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) { @@ -119,7 +119,7 @@ func toCoffeeInfo(b *product.Coffee) (CoffeeInfo, error) { return CoffeeInfo{}, errors.New("beverage is nil") } d := toCoffeeDetails(b.Details()) - return CoffeeInfo{ID: b.AggregateID, Name: b.Type, Price: b.Price(), CuppingScore: b.CoffeeValue().Value, CoffeeDetails: d}, nil + 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 {