service.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. // Package tracking provides the use-case of tracking a shipping. Used by views
  2. // facing the end-user.
  3. package tracking
  4. import (
  5. "errors"
  6. "fmt"
  7. "strings"
  8. "time"
  9. shipping "github.com/longjoy/micro-go-course/section19/cargo/model"
  10. )
  11. // ErrInvalidArgument is returned when one or more arguments are invalid.
  12. var ErrInvalidArgument = errors.New("invalid argument")
  13. // Service is the interface that provides the basic Track method.
  14. type Service interface {
  15. // Track returns a cargo matching a tracking ID.
  16. Track(id string) (Cargo, error)
  17. }
  18. type service struct {
  19. cargos shipping.CargoRepository
  20. handlingEvents shipping.HandlingEventRepository
  21. }
  22. func (s *service) Track(id string) (Cargo, error) {
  23. if id == "" {
  24. return Cargo{}, ErrInvalidArgument
  25. }
  26. c, err := s.cargos.Find(shipping.TrackingID(id))
  27. if err != nil {
  28. return Cargo{}, err
  29. }
  30. return assemble(c, s.handlingEvents), nil
  31. }
  32. // NewService returns a new instance of the default Service.
  33. func NewService(cargos shipping.CargoRepository, events shipping.HandlingEventRepository) Service {
  34. return &service{
  35. cargos: cargos,
  36. handlingEvents: events,
  37. }
  38. }
  39. // Cargo is a read model for tracking views.
  40. type Cargo struct {
  41. TrackingID string `json:"tracking_id"`
  42. StatusText string `json:"status_text"`
  43. Origin string `json:"origin"`
  44. Destination string `json:"destination"`
  45. ETA time.Time `json:"eta"`
  46. NextExpectedActivity string `json:"next_expected_activity"`
  47. ArrivalDeadline time.Time `json:"arrival_deadline"`
  48. Events []Event `json:"events"`
  49. }
  50. // Leg is a read model for booking views.
  51. type Leg struct {
  52. VoyageNumber string `json:"voyage_number"`
  53. From string `json:"from"`
  54. To string `json:"to"`
  55. LoadTime time.Time `json:"load_time"`
  56. UnloadTime time.Time `json:"unload_time"`
  57. }
  58. // Event is a read model for tracking views.
  59. type Event struct {
  60. Description string `json:"description"`
  61. Expected bool `json:"expected"`
  62. }
  63. func assemble(c *shipping.Cargo, events shipping.HandlingEventRepository) Cargo {
  64. return Cargo{
  65. TrackingID: string(c.TrackingID),
  66. Origin: string(c.Origin),
  67. Destination: string(c.RouteSpecification.Destination),
  68. ETA: c.Delivery.ETA,
  69. NextExpectedActivity: nextExpectedActivity(c),
  70. ArrivalDeadline: c.RouteSpecification.ArrivalDeadline,
  71. StatusText: assembleStatusText(c),
  72. Events: assembleEvents(c, events),
  73. }
  74. }
  75. func assembleLegs(c shipping.Cargo) []Leg {
  76. var legs []Leg
  77. for _, l := range c.Itinerary.Legs {
  78. legs = append(legs, Leg{
  79. VoyageNumber: string(l.VoyageNumber),
  80. From: string(l.LoadLocation),
  81. To: string(l.UnloadLocation),
  82. LoadTime: l.LoadTime,
  83. UnloadTime: l.UnloadTime,
  84. })
  85. }
  86. return legs
  87. }
  88. func nextExpectedActivity(c *shipping.Cargo) string {
  89. a := c.Delivery.NextExpectedActivity
  90. prefix := "Next expected activity is to"
  91. switch a.Type {
  92. case shipping.Load:
  93. return fmt.Sprintf("%s %s cargo onto voyage %s in %s.", prefix, strings.ToLower(a.Type.String()), a.VoyageNumber, a.Location)
  94. case shipping.Unload:
  95. return fmt.Sprintf("%s %s cargo off of voyage %s in %s.", prefix, strings.ToLower(a.Type.String()), a.VoyageNumber, a.Location)
  96. case shipping.NotHandled:
  97. return "There are currently no expected activities for this shipping."
  98. }
  99. return fmt.Sprintf("%s %s cargo in %s.", prefix, strings.ToLower(a.Type.String()), a.Location)
  100. }
  101. func assembleStatusText(c *shipping.Cargo) string {
  102. switch c.Delivery.TransportStatus {
  103. case shipping.NotReceived:
  104. return "Not received"
  105. case shipping.InPort:
  106. return fmt.Sprintf("In port %s", c.Delivery.LastKnownLocation)
  107. case shipping.OnboardCarrier:
  108. return fmt.Sprintf("Onboard voyage %s", c.Delivery.CurrentVoyage)
  109. case shipping.Claimed:
  110. return "Claimed"
  111. default:
  112. return "Unknown"
  113. }
  114. }
  115. func assembleEvents(c *shipping.Cargo, handlingEvents shipping.HandlingEventRepository) []Event {
  116. h := handlingEvents.QueryHandlingHistory(c.TrackingID)
  117. var events []Event
  118. for _, e := range h.HandlingEvents {
  119. var description string
  120. switch e.Activity.Type {
  121. case shipping.NotHandled:
  122. description = "Cargo has not yet been received."
  123. case shipping.Receive:
  124. description = fmt.Sprintf("Received in %s, at %s", e.Activity.Location, time.Now().Format(time.RFC3339))
  125. case shipping.Load:
  126. description = fmt.Sprintf("Loaded onto voyage %s in %s, at %s.", e.Activity.VoyageNumber, e.Activity.Location, time.Now().Format(time.RFC3339))
  127. case shipping.Unload:
  128. description = fmt.Sprintf("Unloaded off voyage %s in %s, at %s.", e.Activity.VoyageNumber, e.Activity.Location, time.Now().Format(time.RFC3339))
  129. case shipping.Claim:
  130. description = fmt.Sprintf("Claimed in %s, at %s.", e.Activity.Location, time.Now().Format(time.RFC3339))
  131. case shipping.Customs:
  132. description = fmt.Sprintf("Cleared customs in %s, at %s.", e.Activity.Location, time.Now().Format(time.RFC3339))
  133. default:
  134. description = "[Unknown status]"
  135. }
  136. events = append(events, Event{
  137. Description: description,
  138. Expected: c.Itinerary.IsExpected(e),
  139. })
  140. }
  141. return events
  142. }