| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 | // Package tracking provides the use-case of tracking a shipping. Used by views// facing the end-user.package trackingimport (	"errors"	"fmt"	"strings"	"time"	shipping "github.com/longjoy/micro-go-course/section19/cargo/model")// ErrInvalidArgument is returned when one or more arguments are invalid.var ErrInvalidArgument = errors.New("invalid argument")// Service is the interface that provides the basic Track method.type Service interface {	// Track returns a cargo matching a tracking ID.	Track(id string) (Cargo, error)}type service struct {	cargos         shipping.CargoRepository	handlingEvents shipping.HandlingEventRepository}func (s *service) Track(id string) (Cargo, error) {	if id == "" {		return Cargo{}, ErrInvalidArgument	}	c, err := s.cargos.Find(shipping.TrackingID(id))	if err != nil {		return Cargo{}, err	}	return assemble(c, s.handlingEvents), nil}// NewService returns a new instance of the default Service.func NewService(cargos shipping.CargoRepository, events shipping.HandlingEventRepository) Service {	return &service{		cargos:         cargos,		handlingEvents: events,	}}// Cargo is a read model for tracking views.type Cargo struct {	TrackingID           string    `json:"tracking_id"`	StatusText           string    `json:"status_text"`	Origin               string    `json:"origin"`	Destination          string    `json:"destination"`	ETA                  time.Time `json:"eta"`	NextExpectedActivity string    `json:"next_expected_activity"`	ArrivalDeadline      time.Time `json:"arrival_deadline"`	Events               []Event   `json:"events"`}// Leg is a read model for booking views.type Leg struct {	VoyageNumber string    `json:"voyage_number"`	From         string    `json:"from"`	To           string    `json:"to"`	LoadTime     time.Time `json:"load_time"`	UnloadTime   time.Time `json:"unload_time"`}// Event is a read model for tracking views.type Event struct {	Description string `json:"description"`	Expected    bool   `json:"expected"`}func assemble(c *shipping.Cargo, events shipping.HandlingEventRepository) Cargo {	return Cargo{		TrackingID:           string(c.TrackingID),		Origin:               string(c.Origin),		Destination:          string(c.RouteSpecification.Destination),		ETA:                  c.Delivery.ETA,		NextExpectedActivity: nextExpectedActivity(c),		ArrivalDeadline:      c.RouteSpecification.ArrivalDeadline,		StatusText:           assembleStatusText(c),		Events:               assembleEvents(c, events),	}}func assembleLegs(c shipping.Cargo) []Leg {	var legs []Leg	for _, l := range c.Itinerary.Legs {		legs = append(legs, Leg{			VoyageNumber: string(l.VoyageNumber),			From:         string(l.LoadLocation),			To:           string(l.UnloadLocation),			LoadTime:     l.LoadTime,			UnloadTime:   l.UnloadTime,		})	}	return legs}func nextExpectedActivity(c *shipping.Cargo) string {	a := c.Delivery.NextExpectedActivity	prefix := "Next expected activity is to"	switch a.Type {	case shipping.Load:		return fmt.Sprintf("%s %s cargo onto voyage %s in %s.", prefix, strings.ToLower(a.Type.String()), a.VoyageNumber, a.Location)	case shipping.Unload:		return fmt.Sprintf("%s %s cargo off of voyage %s in %s.", prefix, strings.ToLower(a.Type.String()), a.VoyageNumber, a.Location)	case shipping.NotHandled:		return "There are currently no expected activities for this shipping."	}	return fmt.Sprintf("%s %s cargo in %s.", prefix, strings.ToLower(a.Type.String()), a.Location)}func assembleStatusText(c *shipping.Cargo) string {	switch c.Delivery.TransportStatus {	case shipping.NotReceived:		return "Not received"	case shipping.InPort:		return fmt.Sprintf("In port %s", c.Delivery.LastKnownLocation)	case shipping.OnboardCarrier:		return fmt.Sprintf("Onboard voyage %s", c.Delivery.CurrentVoyage)	case shipping.Claimed:		return "Claimed"	default:		return "Unknown"	}}func assembleEvents(c *shipping.Cargo, handlingEvents shipping.HandlingEventRepository) []Event {	h := handlingEvents.QueryHandlingHistory(c.TrackingID)	var events []Event	for _, e := range h.HandlingEvents {		var description string		switch e.Activity.Type {		case shipping.NotHandled:			description = "Cargo has not yet been received."		case shipping.Receive:			description = fmt.Sprintf("Received in %s, at %s", e.Activity.Location, time.Now().Format(time.RFC3339))		case shipping.Load:			description = fmt.Sprintf("Loaded onto voyage %s in %s, at %s.", e.Activity.VoyageNumber, e.Activity.Location, time.Now().Format(time.RFC3339))		case shipping.Unload:			description = fmt.Sprintf("Unloaded off voyage %s in %s, at %s.", e.Activity.VoyageNumber, e.Activity.Location, time.Now().Format(time.RFC3339))		case shipping.Claim:			description = fmt.Sprintf("Claimed in %s, at %s.", e.Activity.Location, time.Now().Format(time.RFC3339))		case shipping.Customs:			description = fmt.Sprintf("Cleared customs in %s, at %s.", e.Activity.Location, time.Now().Format(time.RFC3339))		default:			description = "[Unknown status]"		}		events = append(events, Event{			Description: description,			Expected:    c.Itinerary.IsExpected(e),		})	}	return events}
 |