Skip to content

Commit f4736a3

Browse files
authored
Implement Match Status and CurrentMinute (#1781)
* Refactor broadcast content state to handle event currentMinute and new phase events. * Update Synthetic Event generator with extra time and penalty shoot out phases * fix SyntheticMatchEventGeneratorSpec * add debug logging to broadcast payloads * update logging * fix * Update BroadcastContentState.scala * Update Broadcast local payload * Add new Match Phase synthetic events for live activities * Remove start live activity event for now * fix test * FOR TEST set football lamdda code to use real time * Revert Code to time machine * Clean up test and print lns. * fix temp debugging logs
1 parent 267cdc2 commit f4736a3

13 files changed

Lines changed: 844 additions & 119 deletions

File tree

api-models/src/main/scala/com/gu/mobile.notifications.client/models/liveActitivites/BroadcastContentState.scala

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,30 +33,57 @@ case object PreMatch extends MatchStatus { val status = "PRE_MATCH" }
3333
case object FirstHalf extends MatchStatus { val status = "FIRST_HALF" }
3434
case object HalfTime extends MatchStatus { val status = "HALF_TIME" }
3535
case object SecondHalf extends MatchStatus { val status = "SECOND_HALF" }
36+
case object ExtraTimeToBePlayed extends MatchStatus { val status = "EXTRA_TIME_TO_BE_PLAYED" }
3637
case object ExtraTimeFirstHalf extends MatchStatus { val status = "EXTRA_TIME_FIRST_HALF" }
3738
case object ExtraTimeHalfTime extends MatchStatus { val status = "EXTRA_TIME_HALF_TIME" }
3839
case object ExtraTimeSecondHalf extends MatchStatus { val status = "EXTRA_TIME_SECOND_HALF" }
40+
case object PenaltiesToBePlayed extends MatchStatus { val status = "PENALTIES_TO_BE_PLAYED" }
3941
case object Penalties extends MatchStatus { val status = "PENALTIES" }
4042
case object FullTime extends MatchStatus { val status = "FULL_TIME" }
4143
case object Postponed extends MatchStatus { val status = "POSTPONED" }
44+
case object Suspended extends MatchStatus { val status = "SUSPENDED" }
4245
case object Abandoned extends MatchStatus { val status = "ABANDONED" }
46+
case object Resumed extends MatchStatus { val status = "RESUMED" }
47+
case object Cancelled extends MatchStatus { val status = "CANCELLED" }
4348
// @formatter:on
4449

4550
object MatchStatus {
46-
def fromString(s: String): MatchStatus = s match {
47-
case "Pre-match" | "prematch" => PreMatch
48-
case "MatchInProgress" | "1st Half" | "first_half" => FirstHalf
49-
case "HalfTime" | "Half Time" | "half_time" => HalfTime
50-
case "2nd Half" | "second_half" => SecondHalf
51-
case "Extra Time" | "ET 1st Half" => ExtraTimeFirstHalf
52-
case "ET Half Time" => ExtraTimeHalfTime
53-
case "ET 2nd Half" => ExtraTimeSecondHalf
54-
case "Penalties" => Penalties
55-
case "MatchComplete" | "MC" | "Full Time" | "Result" => FullTime
56-
case "Postponed" => Postponed
57-
case "Abandoned" => Abandoned
58-
case _ => Scheduled
59-
}
51+
def fromString(s: String): MatchStatus = statuses.getOrElse(s, Scheduled)
52+
53+
// How PA match status are handled here differs from push notification mappings
54+
private val statuses: Map[String, MatchStatus] = Map(
55+
("Fixture", PreMatch), //
56+
("-", PreMatch), // seen in the wild
57+
("New", PreMatch), //
58+
59+
("KO", FirstHalf), // The Match has started (Kicked Off).
60+
61+
("HT", HalfTime), // The Referee has blown the whistle for Half Time.
62+
63+
("SHS", SecondHalf), // The Second Half of the Match has Started.
64+
65+
("FT", FullTime), // The Referee has blown the whistle for Full Time.
66+
("Result", FullTime), // The Result is official.
67+
("MC", FullTime), // Match has been Completed.
68+
69+
("FTET", ExtraTimeToBePlayed), // Full Time, Extra Time it to be played.
70+
("ETS", ExtraTimeFirstHalf), // Extra Time has Started.
71+
("ETHT", ExtraTimeHalfTime), // Extra Time Half Time has been called.
72+
("ETSHS", ExtraTimeSecondHalf), // Extra Time, Second Half has Started.
73+
("ETFT", ExtraTimeSecondHalf), // Extra Time, Full Time has been blown.
74+
75+
("FTPT", PenaltiesToBePlayed), // Full Time, Penalties are To be played.
76+
("ETFTPT", PenaltiesToBePlayed), // Extra Time, Full Time, Penalties are To be played.
77+
("PT", Penalties), // Penalty ShooT Out has started.
78+
("PTFT", Penalties), // Penalty ShooT Full Time.
79+
80+
// edge cases
81+
("Suspended", Suspended), // Match has been Suspended.
82+
("Resumed", Resumed), // Match has been Resumed. // todo can match phase be determined by match minute?
83+
("Abandoned", Abandoned), // Match has been Abandoned.
84+
("Postponed", Postponed), // A Match has been Postponed.
85+
("Cancelled", Cancelled) // A Match has been Cancelled.
86+
)
6087
}
6188

6289
case class Competition(
@@ -105,13 +132,18 @@ object FootballContentJsonFormats {
105132
case "FIRST_HALF" => JsSuccess(FirstHalf)
106133
case "HALF_TIME" => JsSuccess(HalfTime)
107134
case "SECOND_HALF" => JsSuccess(SecondHalf)
135+
case "EXTRA_TIME_TO_BE_PLAYED" => JsSuccess(ExtraTimeToBePlayed)
108136
case "EXTRA_TIME_FIRST_HALF" => JsSuccess(ExtraTimeFirstHalf)
109137
case "EXTRA_TIME_HALF_TIME" => JsSuccess(ExtraTimeHalfTime)
110138
case "EXTRA_TIME_SECOND_HALF" => JsSuccess(ExtraTimeSecondHalf)
139+
case "PENALTIES_TO_BE_PLAYED" => JsSuccess(PenaltiesToBePlayed)
111140
case "PENALTIES" => JsSuccess(Penalties)
112141
case "FULL_TIME" => JsSuccess(FullTime)
142+
case "SUSPENDED" => JsSuccess(Suspended)
143+
case "RESUMED" => JsSuccess(Resumed)
113144
case "POSTPONED" => JsSuccess(Postponed)
114145
case "ABANDONED" => JsSuccess(Abandoned)
146+
case "CANCELLED" => JsSuccess(Cancelled)
115147
case other => JsError(s"Unknown match status: $other")
116148
}
117149
case _ => JsError("Expected a JSON string for MatchStatus")

football/src/main/scala/com/gu/mobile/notifications/football/lib/EventConsumer.scala

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ package com.gu.mobile.notifications.football.lib
33
import com.gu.mobile.notifications.client.models.NotificationPayload
44
import com.gu.mobile.notifications.client.models.liveActitivites.LiveActivityPayload
55
import com.gu.mobile.notifications.football.Logging
6-
import com.gu.mobile.notifications.football.models.{FootballMatchEvent, MatchDataWithArticle}
6+
import com.gu.mobile.notifications.football.models.{
7+
FootballMatchEvent,
8+
MatchDataWithArticle
9+
}
710
import com.gu.mobile.notifications.football.notificationbuilders.{
811
MatchStatusLiveActivityPayloadBuilder,
912
MatchStatusNotificationBuilder
@@ -22,8 +25,31 @@ class EventConsumer(
2225
* to generate push notifications for, so we filter those out here.
2326
*/
2427
// todo can we capture these strings in union type?
25-
// todo we will want to send penalty kick notifications to push notification service for android
26-
val liveActivityEventTypes = List("create-channel", "start-live-activity", "end-live-activity", "shootoutGoal", "shootoutMiss", "shootoutSave")
28+
// todo we will want to send penalty kick notifications and extra synthetic events to push notifications for android
29+
val liveActivityEventTypes =
30+
List(
31+
32+
// penalty shootout events
33+
"shootoutGoal",
34+
"shootoutMiss",
35+
"shootoutSave",
36+
// additional synthetic live activity life cycle events
37+
"create-channel",
38+
"start-live-activity",
39+
"end-live-activity",
40+
// additional synthetic match phase events for live activities
41+
"extra-time-to-be-played",
42+
"extra-time-first-half",
43+
"extra-time-half-time",
44+
"extra-time-second-half",
45+
"penalties-to-be-played",
46+
"penalties",
47+
"suspended",
48+
"resumed",
49+
"abandoned",
50+
"cancelled",
51+
"postponed"
52+
)
2753

2854
val filteredMatchData = matchData.copy(allEvents =
2955
matchData.allEvents.filterNot(e =>

football/src/main/scala/com/gu/mobile/notifications/football/lib/SyntheticMatchEventGenerator.scala

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import pa.{MatchDay, MatchEvent}
55

66
import java.time.{Instant, ZoneId, ZonedDateTime}
77

8+
// Synthetic events create a timeline event for a match status change, that can be processed by the EventConsumer
9+
// and transformed into a NotificationPayload and/or LiveActivityPayload for broadcasting.
10+
811
class SyntheticMatchEventGenerator(getCurrentTime: () => ZonedDateTime) {
912

1013
def generate(events: List[MatchEvent], id: String, matchDay: MatchDay): List[MatchEvent] = {
@@ -39,6 +42,97 @@ class SyntheticMatchEventGenerator(getCurrentTime: () => ZonedDateTime) {
3942
else None
4043
}
4144

45+
46+
// LIVE ACTIVITIES
47+
private val suspended: MatchEventGenerator = { (matchDay: MatchDay, matchEvents: List[pa.MatchEvent]) =>
48+
if (matchDay.matchStatus == "Suspended") Some(emptyMatchEvent.copy(
49+
id = Some(UUID.nameUUIDFromBytes(s"football-match/${matchDay.id}/suspended".getBytes).toString),
50+
eventType = "suspended"
51+
))
52+
else None
53+
}
54+
55+
private val resumed: MatchEventGenerator = { (matchDay: MatchDay, matchEvents: List[pa.MatchEvent]) =>
56+
if (matchDay.matchStatus == "Resumed") Some(emptyMatchEvent.copy(
57+
id = Some(UUID.nameUUIDFromBytes(s"football-match/${matchDay.id}/resumed".getBytes).toString),
58+
eventType = "resumed"
59+
))
60+
else None
61+
}
62+
63+
private val abandoned: MatchEventGenerator = { (matchDay: MatchDay, matchEvents: List[pa.MatchEvent]) =>
64+
if (matchDay.matchStatus == "Abandoned") Some(emptyMatchEvent.copy(
65+
id = Some(UUID.nameUUIDFromBytes(s"football-match/${matchDay.id}/abandoned".getBytes).toString),
66+
eventType = "abandoned"
67+
))
68+
else None
69+
}
70+
71+
private val postponed: MatchEventGenerator = { (matchDay: MatchDay, matchEvents: List[pa.MatchEvent]) =>
72+
if (matchDay.matchStatus == "Postponed") Some(emptyMatchEvent.copy(
73+
id = Some(UUID.nameUUIDFromBytes(s"football-match/${matchDay.id}/postponed".getBytes).toString),
74+
eventType = "postponed"
75+
))
76+
else None
77+
}
78+
79+
private val cancelled: MatchEventGenerator = { (matchDay: MatchDay, matchEvents: List[pa.MatchEvent]) =>
80+
if (matchDay.matchStatus == "Cancelled") Some(emptyMatchEvent.copy(
81+
id = Some(UUID.nameUUIDFromBytes(s"football-match/${matchDay.id}/cancelled".getBytes).toString),
82+
eventType = "cancelled"
83+
))
84+
else None
85+
}
86+
87+
// match statuses synthetic events needed for live activities
88+
private val extraTimeToBePlayed: MatchEventGenerator = { (matchDay: MatchDay, matchEvents: List[pa.MatchEvent]) =>
89+
if (matchDay.matchStatus == "FTET") Some(emptyMatchEvent.copy(
90+
id = Some(UUID.nameUUIDFromBytes(s"football-match/${matchDay.id}/extra-time-to-be-played".getBytes).toString),
91+
eventType = "extra-time-to-be-played"
92+
))
93+
else None
94+
}
95+
96+
private val extraTimeFirstHalf: MatchEventGenerator = { (matchDay: MatchDay, matchEvents: List[pa.MatchEvent]) =>
97+
if (matchDay.matchStatus == "ETS") Some(emptyMatchEvent.copy(
98+
id = Some(UUID.nameUUIDFromBytes(s"football-match/${matchDay.id}/extra-time-first-half".getBytes).toString),
99+
eventType = "extra-time-first-half"
100+
))
101+
else None
102+
}
103+
104+
private val extraTimeHalfTime: MatchEventGenerator = { (matchDay: MatchDay, matchEvents: List[pa.MatchEvent]) =>
105+
if (matchDay.matchStatus == "ETHT") Some(emptyMatchEvent.copy(
106+
id = Some(UUID.nameUUIDFromBytes(s"football-match/${matchDay.id}/extra-time-half-time".getBytes).toString),
107+
eventType = "extra-time-half-time"
108+
))
109+
else None
110+
}
111+
112+
private val extraTimeSecondHalf: MatchEventGenerator = { (matchDay: MatchDay, matchEvents: List[pa.MatchEvent]) =>
113+
if (matchDay.matchStatus == "ETSHS") Some(emptyMatchEvent.copy(
114+
id = Some(UUID.nameUUIDFromBytes(s"football-match/${matchDay.id}/extra-time-second-half".getBytes).toString),
115+
eventType = "extra-time-second-half"
116+
))
117+
else None
118+
}
119+
120+
private val penaltiesToBePlayed: MatchEventGenerator = { (matchDay: MatchDay, matchEvents: List[pa.MatchEvent]) =>
121+
if (matchDay.matchStatus == "FTPT" || matchDay.matchStatus == "ETFTPT") Some(emptyMatchEvent.copy(
122+
id = Some(UUID.nameUUIDFromBytes(s"football-match/${matchDay.id}/penalties-to-be-played".getBytes).toString),
123+
eventType = "penalties-to-be-played"
124+
))
125+
else None
126+
}
127+
128+
private val penalties: MatchEventGenerator = { (matchDay: MatchDay, matchEvents: List[pa.MatchEvent]) =>
129+
if (matchDay.matchStatus == "PT") Some(emptyMatchEvent.copy(
130+
id = Some(UUID.nameUUIDFromBytes(s"football-match/${matchDay.id}/penalties".getBytes).toString),
131+
eventType = "penalties"
132+
))
133+
else None
134+
}
135+
42136
// Live Activity supporting events //
43137

44138
def now: Long = getCurrentTime().toInstant.getEpochSecond
@@ -73,7 +167,24 @@ class SyntheticMatchEventGenerator(getCurrentTime: () => ZonedDateTime) {
73167
else None
74168
}
75169

76-
private val generators: List[MatchEventGenerator] = List(fullTime, halfTime, secondHalf, createChannel, startLiveActivity, endLiveActivity)
170+
private val generators: List[MatchEventGenerator] = List(
171+
fullTime,
172+
halfTime,
173+
secondHalf,
174+
extraTimeToBePlayed,
175+
extraTimeFirstHalf,
176+
extraTimeHalfTime,
177+
extraTimeSecondHalf,
178+
penaltiesToBePlayed,
179+
penalties,
180+
suspended,
181+
resumed,
182+
abandoned,
183+
postponed,
184+
cancelled,
185+
createChannel,
186+
startLiveActivity,
187+
endLiveActivity)
77188

78189
private def emptyMatchEvent = MatchEvent(
79190
id = None,

football/src/main/scala/com/gu/mobile/notifications/football/models/MatchEvents.scala

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.gu.mobile.notifications.football.models
22

3-
import ch.qos.logback.core.model.conditional.ElseModel
43
import com.gu.mobile.notifications.client.models.liveActitivites.PenaltyShootoutState
54
import com.gu.mobile.notifications.client.models.{DefaultGoalType, GoalType, MissedShootoutResult, OwnGoalType, PenaltyGoalType, SavedShootoutResult, ScoredShootoutResult, ShootoutResultType}
65

@@ -129,14 +128,14 @@ object PenaltyShootoutKick {
129128
kickingTeam = if (player.teamID == homeTeam.id) homeTeam else awayTeam
130129
otherTeam = if (player.teamID == homeTeam.id) awayTeam else homeTeam
131130
eventTime <- event.eventTime
132-
eventMinute <- Try(eventTime.toInt).toOption
131+
currentMinute <- Try(eventTime.toInt).toOption
133132
eventId <- event.id
134133
} yield PenaltyShootoutKick(
135134
result,
136135
player.name,
137136
kickingTeam,
138137
otherTeam,
139-
eventMinute,
138+
currentMinute,
140139
eventId
141140
)
142141

@@ -172,19 +171,34 @@ object PenaltyShootoutScore {
172171
case class PenaltyShootoutScore(homeScored: Int = 0, homeMissed: Int = 0, homeSaved: Int = 0, awayScored: Int = 0, awayMissed: Int = 0, awaySaved: Int = 0)
173172

174173

175-
trait MatchPhaseEvent extends FootballMatchEvent
174+
trait MatchPhaseEvent extends FootballMatchEvent {
175+
val currentMinute: Option[Int] = None
176+
}
176177

177178
object MatchPhaseEvent {
179+
// some of these are Synthetic Events we generate and add to the timeline.
178180
def fromEvent(event: pa.MatchEvent): Option[MatchPhaseEvent] = {
179181
val eventId = event.id.getOrElse("")
180182
condOpt(event.eventType) {
181183
case "timeline" if event.matchTime.contains("0:00") => KickOff(eventId)
182-
case "full-time" => FullTime(eventId) // todo use this to end activity?
184+
case "full-time" => FullTime(eventId)
183185
case "half-time" => HalfTime(eventId)
184186
case "second-half" => SecondHalf(eventId)
187+
case "extra-time-to-be-played" => ExtraTimeToBePlayed(eventId)
188+
case "extra-time-first-half" => ExtraTimeFirstHalf(eventId)
189+
case "extra-time-half-time" => ExtraTimeHalfTime(eventId)
190+
case "extra-time-second-half" => ExtraTimeSecondHalf(eventId)
191+
case "penalties-to-be-played" => PenaltiesToBePlayed(eventId)
192+
case "penalties" => Penalties(eventId)
193+
case "suspended" => Suspended(eventId)
194+
case "resumed" => Resumed(eventId)
195+
case "abandoned" => Abandoned(eventId)
196+
case "postponed" => Postponed(eventId)
197+
case "cancelled" => Cancelled(eventId)
185198
case "create-channel" => CreateChannel(eventId)
186199
case "start-live-activity" => StartLiveActivity(eventId)
187200
case "end-live-activity" => EndLiveActivity(eventId)
201+
188202
}
189203
}
190204
}
@@ -193,6 +207,20 @@ case class KickOff(eventId: String) extends MatchPhaseEvent
193207
case class FullTime(eventId: String) extends MatchPhaseEvent
194208
case class HalfTime(eventId: String) extends MatchPhaseEvent
195209
case class SecondHalf(eventId: String) extends MatchPhaseEvent
210+
case class ExtraTimeToBePlayed(eventId: String) extends MatchPhaseEvent
211+
case class ExtraTimeFirstHalf(eventId: String) extends MatchPhaseEvent
212+
case class ExtraTimeHalfTime(eventId: String) extends MatchPhaseEvent
213+
case class ExtraTimeSecondHalf(eventId: String) extends MatchPhaseEvent
214+
case class PenaltiesToBePlayed(eventId: String) extends MatchPhaseEvent
215+
case class Penalties(eventId: String) extends MatchPhaseEvent
216+
// extraordinary events
217+
case class Suspended(eventId: String) extends MatchPhaseEvent
218+
case class Resumed(eventId: String) extends MatchPhaseEvent
219+
case class Abandoned(eventId: String) extends MatchPhaseEvent
220+
case class Postponed(eventId: String) extends MatchPhaseEvent
221+
case class Cancelled(eventId: String) extends MatchPhaseEvent
222+
223+
// Live Activity phase events
196224
case class CreateChannel(eventId: String) extends MatchPhaseEvent
197225
case class StartLiveActivity(eventId: String) extends MatchPhaseEvent
198-
case class EndLiveActivity(eventId: String) extends MatchPhaseEvent
226+
case class EndLiveActivity(eventId: String) extends MatchPhaseEvent // occurs when there is a result

0 commit comments

Comments
 (0)