Skip to content

Commit

Permalink
Begin adding time zone to time entries
Browse files Browse the repository at this point in the history
  • Loading branch information
dani-agilogy committed May 26, 2023
1 parent 6dcb3ef commit 7bdb989
Show file tree
Hide file tree
Showing 8 changed files with 46 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ class ConsoleAdapter(
timeTrackingApp.listTimeEntries(cmd.yearMonth.toLocalDateRange(), cmd.developer),
)

is AddTimeEntry -> timeTrackingApp.saveTimeEntries(cmd.developer, listOf(cmd.project to cmd.range))
is AddTimeEntry -> //timeTrackingApp.saveTimeEntries(cmd.developer, listOf(cmd.project to cmd.range))
TODO("Not implemented: missing time zone id")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ package com.agilogy.timetracking.domain

import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import kotlin.time.Duration
import kotlin.time.toKotlinDuration

data class TimeEntry(val developer: DeveloperName, val project: ProjectName, val range: ClosedRange<Instant>) {
data class TimeEntry(
val developer: DeveloperName,
val project: ProjectName,
val range: ClosedRange<Instant>,
val zoneId: ZoneId
) {
val duration: Duration = java.time.Duration.between(range.start, range.endInclusive.plusNanos(1)).toKotlinDuration()
val localDate: LocalDate by lazy { range.start.localDate() }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import arrow.core.Tuple4
import java.time.Instant
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZoneId

interface TimeTrackingApp {

suspend fun saveTimeEntries(developer: DeveloperName, timeEntries: List<Pair<ProjectName, ClosedRange<Instant>>>)
suspend fun saveTimeEntries(developer: DeveloperName, timeEntries: List<Triple<ProjectName, ClosedRange<Instant>, ZoneId>>)
suspend fun getDeveloperHours(range: ClosedRange<Instant>): Map<Pair<DeveloperName, ProjectName>, Hours>
suspend fun getDeveloperHoursByProjectAndDate(developer: DeveloperName, dateRange: ClosedRange<LocalDate>):
List<Triple<LocalDate, ProjectName, Hours>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import arrow.core.Tuple4
import java.time.Instant
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZoneId

// TODO: Redesign TimeTrackingAppPrd dates:
// - Report parameters should use LocalDate, Month, etc. and a(n optional?) user timezone
Expand All @@ -26,9 +27,9 @@ class TimeTrackingAppPrd(private val timeEntriesRepository: TimeEntriesRepositor
// - Merge together consecutive time entries
override suspend fun saveTimeEntries(
developer: DeveloperName,
timeEntries: List<Pair<ProjectName, ClosedRange<Instant>>>,
timeEntries: List<Triple<ProjectName, ClosedRange<Instant>, ZoneId>>,
) {
timeEntriesRepository.saveTimeEntries(timeEntries.map { TimeEntry(developer, it.first, it.second) })
timeEntriesRepository.saveTimeEntries(timeEntries.map { TimeEntry(developer, it.first, it.second, it.third) })
}

// TODO: Redesign getDeveloperHours to take a ClosedRange<LocalDate> and a timezone
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.agilogy.timetracking.domain.test.InMemoryTimeEntriesRepository
import io.kotest.core.spec.style.FunSpec
import org.junit.jupiter.api.Assertions.assertEquals
import java.time.Instant
import java.time.ZoneId

class TimeTrackingAppTest : FunSpec() {
init {
Expand All @@ -12,18 +13,24 @@ class TimeTrackingAppTest : FunSpec() {
val start = now.minusSeconds(hours * 3600L)
val developer = DeveloperName("John")
val project = ProjectName("Acme Inc.")
val zoneId: ZoneId = ZoneId.of("Australia/Sydney")

test("Save time entries") {
val timeEntriesRepository = InMemoryTimeEntriesRepository()
val app = TimeTrackingAppPrd(timeEntriesRepository)
val developerTimeEntries = listOf(project to start..now)
val developerTimeEntries = listOf(Triple(project, start..now, zoneId))
app.saveTimeEntries(developer, developerTimeEntries)
val expected = listOf(TimeEntry(developer, project, start..now))
val expected = listOf(TimeEntry(developer, project, start..now, zoneId))
assertEquals(expected, timeEntriesRepository.getState())
}

test("Get hours per developer") {
val timeEntriesRepository = InMemoryTimeEntriesRepository(listOf(TimeEntry(developer, project, start..now)))
val timeEntriesRepository = InMemoryTimeEntriesRepository(listOf(TimeEntry(
developer,
project,
start..now,
zoneId
)))
val app = TimeTrackingAppPrd(timeEntriesRepository)
val result = app.getDeveloperHours(start..now)
val expected = mapOf((developer to project) to Hours(hours))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import io.ktor.server.testing.testApplication
import org.junit.jupiter.api.Assertions.assertEquals
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.temporal.ChronoUnit

class TimeTrackingApiTest : FunSpec() {
Expand Down Expand Up @@ -55,7 +56,8 @@ class TimeTrackingApiTest : FunSpec() {
test("return OK with a not empty list of entries") {
val startTime = now.minus(2, ChronoUnit.HOURS)
val startDate = startTime.localDate()
withApp(TimeEntry(john, agilogySchool, startTime..now)) { client ->
val zoneId = ZoneId.of("Australia/Sydney")
withApp(TimeEntry(john, agilogySchool, startTime..now, zoneId)) { client ->
val response =
client.get(
"/time-entries/daily-user-hours?" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.agilogy.timetracking.domain.TimeEntry
import com.agilogy.timetracking.domain.toInstantRange
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import javax.sql.DataSource
import kotlin.math.ceil

Expand All @@ -37,6 +38,8 @@ class PostgresTimeEntriesRepository(private val dataSource: DataSource) : TimeEn
|"end" timestamptz not null
)
""".trimMargin(),
"""alter table time_entries add column zone_id text not null default 'Europe/Madrid'""",
"""alter table time_entries alter column zone_id drop default"""
)
}

Expand Down Expand Up @@ -93,12 +96,12 @@ class PostgresTimeEntriesRepository(private val dataSource: DataSource) : TimeEn

override suspend fun listTimeEntries(timeRange: ClosedRange<Instant>, developer: DeveloperName?): List<TimeEntry> =
dataSource.sql {
val sql = """select developer, project, start, "end"
val sql = """select developer, project, start, "end", zone_id
|from time_entries
|where "end" > ? and start < ?
""".trimMargin()
select(sql, timeRange.start.param, timeRange.endInclusive.param) {
TimeEntry(it.developer(1)!!, it.project(2)!!, it.timestamp(3)!!..it.timestamp(4)!!)
TimeEntry(it.developer(1)!!, it.project(2)!!, it.timestamp(3)!!..it.timestamp(4)!!, ZoneId.of(it.string(5)!!))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@ class TimeEntriesRepositoryTest : FunSpec() {
val testDay = date(1)
repo.saveTimeEntries(
listOf(
TimeEntry(d1, p, timePeriod(1, 9, 4)),
TimeEntry(d1, p, timePeriod(1, 14, 3)),
TimeEntry(d2, p, timePeriod(1, 10, 3)),
TimeEntry(d1, p, timePeriod(1, 9, 4), zoneId),
TimeEntry(d1, p, timePeriod(1, 14, 3), zoneId),
TimeEntry(d2, p, timePeriod(1, 10, 3), zoneId),
),
)
assertEquals(
Expand All @@ -119,21 +119,21 @@ class TimeEntriesRepositoryTest : FunSpec() {
}

test("Get hours per developer") { repo ->
repo.saveTimeEntries(listOf(TimeEntry(developer, project, start..now)))
repo.saveTimeEntries(listOf(TimeEntry(developer, project, start..now, zoneId)))
val result = repo.getHoursByDeveloperAndProject(start..now)
val expected = mapOf((developer to project) to Hours(hours))
assertEquals(expected, result)
}

test("Get hours per developer when range is bigger than the developer hours") { repo ->
repo.saveTimeEntries(listOf(TimeEntry(developer, project, start..now)))
repo.saveTimeEntries(listOf(TimeEntry(developer, project, start..now, zoneId)))
val result = repo.getHoursByDeveloperAndProject(start.minusSeconds(7200L)..now.plusSeconds(7200L))
val expected = mapOf((developer to project) to Hours(1))
assertEquals(expected, result)
}

test("Get hours per developer when range makes no sense") { repo ->
repo.saveTimeEntries(listOf(TimeEntry(developer, project, start..now)))
repo.saveTimeEntries(listOf(TimeEntry(developer, project, start..now, zoneId)))
val resultOutside = repo.getHoursByDeveloperAndProject(start.plusSeconds(7200L)..now.minusSeconds(7200L))
val resultInside = repo.getHoursByDeveloperAndProject(start.plusSeconds(2700L)..now.minusSeconds(2700L))
val expected = emptyMap<Pair<DeveloperName, ProjectName>, Hours>()
Expand All @@ -142,21 +142,21 @@ class TimeEntriesRepositoryTest : FunSpec() {
}

test("getHoursByDeveloperAndProject returns the hours in the interval") { repo ->
repo.saveTimeEntries(listOf(TimeEntry(developer, project, at(9)..at(13))))
repo.saveTimeEntries(listOf(TimeEntry(developer, project, at(9)..at(13), zoneId)))
val actual = repo.getHoursByDeveloperAndProject(at(10)..at(12))
val expected = mapOf((developer to project) to Hours(2))
assertEquals(expected, actual)
}

test("getHoursByDeveloperAndProject rounds properly up") { repo ->
repo.saveTimeEntries(listOf(TimeEntry(developer, project, at(10)..at(10, 30))))
repo.saveTimeEntries(listOf(TimeEntry(developer, project, at(10)..at(10, 30), zoneId)))
val result = repo.getHoursByDeveloperAndProject(at(10)..at(11))
val expected = mapOf((developer to project) to Hours(1))
assertEquals(expected, result)
}

test("Get hours per developer when range is outside the developer hours") { repo ->
repo.saveTimeEntries(listOf(TimeEntry(developer, project, start..now)))
repo.saveTimeEntries(listOf(TimeEntry(developer, project, start..now, zoneId)))
val resultLeft = repo.getHoursByDeveloperAndProject(start.minusSeconds(3600L)..now.minusSeconds(7200L))
val resultRight = repo.getHoursByDeveloperAndProject(start.plusSeconds(7200L)..now.plusSeconds(3600L))
val expected = emptyMap<Pair<DeveloperName, ProjectName>, Hours>()
Expand All @@ -165,7 +165,7 @@ class TimeEntriesRepositoryTest : FunSpec() {
}

test("Get hours per developer when only one part of the range is inside") { repo ->
repo.saveTimeEntries(listOf(TimeEntry(developer, project, start..now)))
repo.saveTimeEntries(listOf(TimeEntry(developer, project, start..now, zoneId)))
val resultStartInsideEndOutside = repo.getHoursByDeveloperAndProject(
start.plusSeconds(1600L)..now.plusSeconds(1600L),
)
Expand All @@ -180,10 +180,10 @@ class TimeEntriesRepositoryTest : FunSpec() {
test("getDeveloperHoursByProjectAndDate") { repo ->
repo.saveTimeEntries(
listOf(
TimeEntry(d1, p, timePeriod(1, 9, 1)),
TimeEntry(d1, p, timePeriod(1, 11, 2)),
TimeEntry(d1, p2, timePeriod(1, 14, 4)),
TimeEntry(d1, p, timePeriod(2, 8, 6)),
TimeEntry(d1, p, timePeriod(1, 9, 1), zoneId),
TimeEntry(d1, p, timePeriod(1, 11, 2), zoneId),
TimeEntry(d1, p2, timePeriod(1, 14, 4), zoneId),
TimeEntry(d1, p, timePeriod(2, 8, 6), zoneId),
),
)

Expand Down

0 comments on commit 7bdb989

Please sign in to comment.