Skip to content

Commit

Permalink
The API that creates the schedule must know about the user time zone.…
Browse files Browse the repository at this point in the history
… Because for the same user in different time zones the schedule is different.
  • Loading branch information
yaskovdev committed Aug 25, 2024
1 parent 0e15a7d commit b9a4b61
Show file tree
Hide file tree
Showing 14 changed files with 93 additions and 63 deletions.
4 changes: 2 additions & 2 deletions src/main/kotlin/org/motivepick/domain/model/Schedule.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package org.motivepick.domain.model

import java.time.LocalDateTime
import java.time.ZonedDateTime

data class Schedule(val week: Map<LocalDateTime, List<Task>>, val overdue: List<Task>, val future: List<Task>)
data class Schedule(val week: Map<ZonedDateTime, List<ScheduledTask>>, val overdue: List<ScheduledTask>, val future: List<ScheduledTask>)
11 changes: 11 additions & 0 deletions src/main/kotlin/org/motivepick/domain/model/ScheduledTask.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.motivepick.domain.model

import org.motivepick.domain.entity.TaskEntity
import java.time.LocalDateTime

data class ScheduledTask(val id: Long, val name: String, val description: String, val dueDate: LocalDateTime, val closed: Boolean) {

companion object {
fun from(entity: TaskEntity): ScheduledTask = ScheduledTask(entity.id, entity.name, entity.description ?: "", entity.dueDate!!, entity.closed)
}
}
4 changes: 2 additions & 2 deletions src/main/kotlin/org/motivepick/domain/view/ScheduleView.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package org.motivepick.domain.view

import java.time.OffsetDateTime
import java.time.ZonedDateTime

data class ScheduleView(val week: Map<OffsetDateTime, List<TaskView>>, val overdue: List<TaskView>, val future: List<TaskView>)
data class ScheduleView(val week: Map<ZonedDateTime, List<ScheduledTaskView>>, val overdue: List<ScheduledTaskView>, val future: List<ScheduledTaskView>)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.motivepick.domain.view

import java.time.OffsetDateTime

data class ScheduledTaskView(val id: Long, val name: String, val description: String, val dueDate: OffsetDateTime, val closed: Boolean)
10 changes: 10 additions & 0 deletions src/main/kotlin/org/motivepick/extensions/ClockExtensions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.motivepick.extensions

import java.time.Clock
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit.DAYS

object ClockExtensions {

fun Clock.endOfToday(): ZonedDateTime = ZonedDateTime.now(this).truncatedTo(DAYS).plusDays(1).minusNanos(1)
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ package org.motivepick.extensions

import org.motivepick.domain.model.Schedule
import org.motivepick.domain.view.ScheduleView
import org.motivepick.extensions.TaskExtensions.view
import java.time.ZoneOffset
import org.motivepick.extensions.ScheduledTaskExtensions.view

object ScheduleExtensions {

fun Schedule.view(): ScheduleView =
ScheduleView(week.map { (k, v) -> k.atOffset(ZoneOffset.UTC) to v.map { it.view() } }.toMap(), overdue.map { it.view() }, future.map { it.view() })
ScheduleView(week.mapValues { entity -> entity.value.map { it.view() } }, overdue.map { it.view() }, future.map { it.view() })
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.motivepick.extensions

import org.motivepick.domain.model.ScheduledTask
import org.motivepick.domain.view.ScheduledTaskView
import java.time.ZoneOffset

object ScheduledTaskExtensions {

fun ScheduledTask.view(): ScheduledTaskView =
ScheduledTaskView(this.id, this.name, this.description, this.dueDate.atOffset(ZoneOffset.UTC), this.closed)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package org.motivepick.extensions

import org.motivepick.domain.entity.TaskEntity
import org.motivepick.domain.model.Task
import org.motivepick.domain.view.TaskView
import java.time.ZoneOffset

internal object TaskEntityExtensions {

fun TaskEntity.model(): Task = Task(this.id, this.name, this.description ?: "", this.dueDate, this.closed)

fun TaskEntity.view(): TaskView = TaskView(this.id, this.name, this.description ?: "", this.dueDate?.atOffset(ZoneOffset.UTC), this.closed)
}
54 changes: 21 additions & 33 deletions src/main/kotlin/org/motivepick/service/ScheduleFactory.kt
Original file line number Diff line number Diff line change
@@ -1,47 +1,35 @@
package org.motivepick.service

import org.motivepick.domain.model.Schedule
import org.motivepick.domain.model.Task
import org.motivepick.extensions.LocalDateTimeExtensions.isSameDayAs
import org.motivepick.domain.model.ScheduledTask
import org.motivepick.extensions.ClockExtensions.endOfToday
import org.springframework.stereotype.Component
import java.time.Clock
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime.MAX
import kotlin.collections.set
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit.DAYS

@Component
class ScheduleFactory(private val clock: Clock) {
class ScheduleFactory {

fun scheduleFor(tasksWithDueDate: List<Task>): Schedule {
val week: MutableMap<LocalDateTime, List<Task>> = week()
for (dayOfWeek in week.keys) {
val tasksOfTheDay = tasksWithDueDate.filter { dayOfWeek.isSameDayAs(it.dueDate!!) }
week[dayOfWeek] = tasksOfTheDay
}
val startOfToday = LocalDate.now(clock).atStartOfDay()
val overdue = tasksWithDueDate.filter { it.dueDate!!.isBefore(startOfToday) }

val startOfFuture = startOfToday.plusDays(7)

return tasksWithDueDate.asSequence()
.filter { it.dueDate!!.isAfter(startOfFuture) }
.sortedBy { it.dueDate }
.firstOrNull()
?.let { task -> tasksWithDueDate.filter { task.dueDate!!.isSameDayAs(it.dueDate!!) } }
?.let { Schedule(week, overdue, it) }
?: Schedule(week, overdue, listOf())
fun scheduleFor(tasksWithDueDate: List<ScheduledTask>, timeZone: ZoneId): Schedule {
val endOfToday = Clock.system(timeZone).endOfToday()
val week: Map<ZonedDateTime, List<ScheduledTask>> = weekDays(endOfToday)
.associateWith { endOfDay -> tasksWithDueDate.filter { it.dueDate.atZone(timeZone) in startOfDayFrom(endOfDay)..endOfDay } }
val startOfToday = startOfDayFrom(endOfToday)
val overdue = tasksWithDueDate.filter { it.dueDate.atZone(timeZone).isBefore(startOfToday) }
val endOfWeek = endOfToday.plusDays(6)
val futureTasks = tasksWithDueDate.filter { it.dueDate.atZone(timeZone).isAfter(endOfWeek) }.sortedBy { it.dueDate }
return Schedule(week, overdue, futureTasks)
}

private fun week(): MutableMap<LocalDateTime, List<Task>> =
LocalDate
.now(clock)
.atTime(MAX)
.let { endOfToday ->
private fun weekDays(endOfToday: ZonedDateTime): List<ZonedDateTime> =
endOfToday
.let { value ->
(0..6)
.map { it.toLong() }
.map { endOfToday.plusDays(it) }
.associateBy({ it }, { ArrayList<Task>() })
.toMutableMap()
.map { value.plusDays(it) }
}

private fun startOfDayFrom(dateTime: ZonedDateTime): ZonedDateTime = dateTime.truncatedTo(DAYS)
}
7 changes: 4 additions & 3 deletions src/main/kotlin/org/motivepick/service/TaskService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package org.motivepick.service

import org.motivepick.domain.entity.TaskListType
import org.motivepick.domain.entity.UserEntity
import org.motivepick.domain.view.ScheduleView
import org.motivepick.domain.view.CreateTaskRequest
import org.motivepick.domain.view.UpdateTaskRequest
import org.motivepick.domain.view.ScheduleView
import org.motivepick.domain.view.TaskView
import org.motivepick.domain.view.UpdateTaskRequest
import org.springframework.data.domain.Page
import java.time.ZoneId

interface TaskService {

Expand All @@ -18,7 +19,7 @@ interface TaskService {

fun findForCurrentUser(listType: TaskListType, offset: Long, limit: Int): Page<TaskView>

fun findScheduleForCurrentUser(): ScheduleView
fun findScheduleForCurrentUser(timeZone: ZoneId): ScheduleView

fun createTask(request: CreateTaskRequest): TaskView

Expand Down
9 changes: 5 additions & 4 deletions src/main/kotlin/org/motivepick/service/TaskServiceImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import org.motivepick.domain.entity.TaskListType
import org.motivepick.domain.entity.TaskListType.CLOSED
import org.motivepick.domain.entity.TaskListType.INBOX
import org.motivepick.domain.entity.UserEntity
import org.motivepick.domain.model.ScheduledTask
import org.motivepick.domain.view.CreateTaskRequest
import org.motivepick.domain.view.ScheduleView
import org.motivepick.domain.view.TaskView
import org.motivepick.domain.view.UpdateTaskRequest
import org.motivepick.extensions.ListExtensions.withPageable
import org.motivepick.extensions.ScheduleExtensions.view
import org.motivepick.extensions.TaskEntityExtensions.model
import org.motivepick.extensions.TaskEntityExtensions.view
import org.motivepick.repository.TaskListRepository
import org.motivepick.repository.TaskRepository
Expand All @@ -25,6 +25,7 @@ import org.springframework.data.domain.Page
import org.springframework.data.domain.PageImpl
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.ZoneId
import kotlin.jvm.optionals.getOrNull

@Service
Expand Down Expand Up @@ -94,10 +95,10 @@ internal class TaskServiceImpl(
}

@Transactional
override fun findScheduleForCurrentUser(): ScheduleView =
override fun findScheduleForCurrentUser(timeZone: ZoneId): ScheduleView =
taskRepository.findAllByUserAccountIdAndClosedFalseAndDueDateNotNullAndVisibleTrue(currentUser.getAccountId())
.map { it.model() }
.let(scheduleFactory::scheduleFor)
.map { ScheduledTask.from(it) }
.let { scheduleFactory.scheduleFor(it, timeZone) }
.view()

@Transactional
Expand Down
3 changes: 2 additions & 1 deletion src/main/kotlin/org/motivepick/web/ScheduleController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import org.springframework.http.ResponseEntity
import org.springframework.http.ResponseEntity.ok
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import java.time.ZoneId

@RestController
internal class ScheduleController(private val taskService: TaskService) {

@GetMapping("/schedule")
fun schedule(): ResponseEntity<ScheduleView> = ok(taskService.findScheduleForCurrentUser())
fun schedule(timeZone: ZoneId?): ResponseEntity<ScheduleView> = ok(taskService.findScheduleForCurrentUser(timeZone ?: ZoneId.of("UTC")))
}
18 changes: 18 additions & 0 deletions src/test/kotlin/org/motivepick/extensions/ClockExtensionsTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.motivepick.extensions

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.motivepick.extensions.ClockExtensions.endOfToday
import java.time.Clock
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime

class ClockExtensionsTest {

@Test
fun `endOfToday should return the end of the day in the given zone`() {
val clock = Clock.fixed(Instant.parse("2024-08-25T11:52:00.00Z"), ZoneId.of("GMT+3"))
assertThat(clock.endOfToday()).isEqualTo(ZonedDateTime.parse("2024-08-25T23:59:59.999999999+03:00"))
}
}

0 comments on commit b9a4b61

Please sign in to comment.