Skip to content

Commit b4b2740

Browse files
committed
feat/qg-253: WIP: события в расписании из Google календаря приведены к таймзоне терапевта
1 parent 8e71047 commit b4b2740

8 files changed

Lines changed: 122 additions & 15 deletions

File tree

app/src/main/kotlin/pro/qyoga/app/therapist/oauth2/GoogleCallbackController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class GoogleOAuthController(
4444
.body(Map::class.java)
4545
val email = response
4646
?.get("email") as String
47-
val picture = response["picture"] as String?
47+
val picture = response["picture"] as? String?
4848

4949
googleCalendarsService.addGoogleAccount(
5050
GoogleAccount(therapistId, email, authorizedClient.refreshToken!!.tokenValue)
Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
11
package pro.qyoga.core.calendar.google
22

3+
import pro.azhidkov.platform.java.time.toLocalDateTime
34
import pro.qyoga.core.calendar.api.CalendarItem
45
import java.time.Duration
5-
import java.time.LocalDateTime
6+
import java.time.ZoneId
7+
import java.time.temporal.Temporal
68

79
@JvmInline
810
value class GoogleCalendarItemId(val value: String)
911

10-
data class GoogleCalendarItem(
12+
data class GoogleCalendarItem<DATE : Temporal>(
1113
override val id: GoogleCalendarItemId,
1214
override val title: String,
1315
override val description: String,
14-
override val dateTime: LocalDateTime,
16+
override val dateTime: DATE,
1517
override val duration: Duration,
1618
override val location: String?
17-
) : CalendarItem<GoogleCalendarItemId, LocalDateTime>
19+
) : CalendarItem<GoogleCalendarItemId, DATE> {
20+
21+
fun toLocalizedCalendarItem(zoneId: ZoneId) =
22+
GoogleCalendarItem(
23+
id,
24+
title,
25+
description,
26+
dateTime.toLocalDateTime(zoneId),
27+
duration,
28+
location
29+
)
30+
31+
}

app/src/main/kotlin/pro/qyoga/core/calendar/google/GoogleCalendarsClient.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import org.springframework.stereotype.Component
1212
import pro.azhidkov.platform.java.time.Interval
1313
import pro.qyoga.core.users.therapists.TherapistRef
1414
import java.net.URI
15-
import java.time.*
15+
import java.time.Duration
16+
import java.time.Instant
17+
import java.time.ZoneId
18+
import java.time.ZonedDateTime
1619

1720

1821
@Component
@@ -33,7 +36,7 @@ class GoogleCalendarsClient(
3336
account: GoogleAccount,
3437
calendarSettings: GoogleCalendarSettings,
3538
interval: Interval<ZonedDateTime>
36-
): List<GoogleCalendarItem> {
39+
): List<GoogleCalendarItem<ZonedDateTime>> {
3740
val service = servicesCache.getValue(account)
3841
val events =
3942
service.events().list(calendarSettings.calendarId)
@@ -56,11 +59,11 @@ class GoogleCalendarsClient(
5659
return events
5760
}
5861

59-
private fun startDate(event: Event): LocalDateTime =
62+
private fun startDate(event: Event): ZonedDateTime =
6063
ZonedDateTime.ofInstant(
6164
Instant.ofEpochMilli(event.start.dateTime?.value ?: event.start.date?.value ?: 0),
6265
ZoneId.of(event.start.timeZone)
63-
).toLocalDateTime()
66+
)
6467

6568
private fun duration(event: Event): Duration =
6669
Duration.ofMillis(event.end.dateTime?.value ?: event.end.date?.value ?: 0) -

app/src/main/kotlin/pro/qyoga/core/calendar/google/GoogleCalendarsService.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.google.api.client.http.javanet.NetHttpTransport
55
import com.google.api.client.json.gson.GsonFactory
66
import org.springframework.stereotype.Service
77
import pro.azhidkov.platform.java.time.Interval
8+
import pro.azhidkov.platform.java.time.zoneId
89
import pro.azhidkov.platform.spring.sdj.ergo.hydration.ref
910
import pro.qyoga.core.calendar.api.CalendarItem
1011
import pro.qyoga.core.calendar.api.CalendarsService
@@ -96,6 +97,7 @@ class GoogleCalendarsService(
9697
googleCalendarsClient.getEvents(account, calendarSettings, interval)
9798
}
9899
}
100+
.map { it.toLocalizedCalendarItem(interval.zoneId) }
99101

100102
return events
101103
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package pro.qyoga.tests.cases.i9ns.calendar.google
2+
3+
import io.kotest.core.annotation.DisplayName
4+
import io.kotest.matchers.collections.shouldHaveSize
5+
import io.kotest.matchers.shouldBe
6+
import pro.azhidkov.platform.java.time.Interval
7+
import pro.qyoga.core.calendar.google.GoogleCalendarsService
8+
import pro.qyoga.tests.fixture.data.asiaNovosibirskTimeZone
9+
import pro.qyoga.tests.fixture.object_mothers.calendars.google.GoogleCalendarObjectMother
10+
import pro.qyoga.tests.fixture.object_mothers.calendars.google.GoogleCalendarObjectMother.aGoogleCalendarItem
11+
import pro.qyoga.tests.fixture.object_mothers.therapists.THE_THERAPIST_REF
12+
import pro.qyoga.tests.fixture.presets.googleCalendarFixturePresets
13+
import pro.qyoga.tests.infra.test_config.spring.context
14+
import pro.qyoga.tests.infra.web.QYogaAppIntegrationBaseKoTest
15+
import java.time.Duration
16+
import java.time.LocalDateTime
17+
import java.time.ZoneId
18+
import java.time.ZonedDateTime
19+
20+
21+
@DisplayName("Сервис Google календарей")
22+
class GoogleCalendarsServiceTest : QYogaAppIntegrationBaseKoTest({
23+
24+
val googleCalendarsService = getBean<GoogleCalendarsService>()
25+
val googleCalendarFixturePresets = context.googleCalendarFixturePresets()
26+
27+
"метод получения событий в интервале" - {
28+
29+
"должен возвращать события приведённые к таймзоне запрошенного интервала" {
30+
// Сетап
31+
googleCalendarFixturePresets.setupCalendar(
32+
THE_THERAPIST_REF, GoogleCalendarObjectMother.aCalendarName(),
33+
aGoogleCalendarItem(
34+
date = { ZonedDateTime.of(2025, 9, 16, 6, 0, 0, 0, ZoneId.of("Europe/Moscow")) },
35+
duration = Duration.ofMinutes(60)
36+
),
37+
shouldBeShown = true
38+
)
39+
40+
// Действие
41+
val items = googleCalendarsService.findCalendarItemsInInterval(
42+
THE_THERAPIST_REF,
43+
Interval.of(ZonedDateTime.of(2025, 9, 16, 0, 0, 0, 0, asiaNovosibirskTimeZone), Duration.ofDays(1))
44+
)
45+
46+
// Проверка
47+
items shouldHaveSize 1
48+
items.first().dateTime shouldBe LocalDateTime.of(2025, 9, 16, 10, 0, 0, 0)
49+
items.first().endDateTime shouldBe LocalDateTime.of(2025, 9, 16, 11, 0, 0, 0)
50+
}
51+
}
52+
53+
})

app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/object_mothers/calendars/google/GoogleCalendarObjectMother.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
package pro.qyoga.tests.fixture.object_mothers.calendars.google
22

33
import pro.qyoga.core.calendar.google.GoogleCalendar
4+
import pro.qyoga.core.calendar.google.GoogleCalendarItem
5+
import pro.qyoga.core.calendar.google.GoogleCalendarItemId
46
import pro.qyoga.core.users.therapists.TherapistRef
57
import pro.qyoga.tests.fixture.data.faker
68
import pro.qyoga.tests.fixture.data.randomElementOf
9+
import pro.qyoga.tests.fixture.object_mothers.appointments.randomAppointmentDuration
10+
import pro.qyoga.tests.fixture.object_mothers.calendars.CalendarsObjectMother.aAppointmentEventTitle
11+
import java.time.Duration
12+
import java.time.temporal.Temporal
713

814

915
object GoogleCalendarObjectMother {
@@ -18,6 +24,19 @@ object GoogleCalendarObjectMother {
1824
name,
1925
)
2026

27+
fun <DATE : Temporal> aGoogleCalendarItem(
28+
date: () -> DATE,
29+
duration: Duration = randomAppointmentDuration()
30+
) =
31+
GoogleCalendarItem(
32+
GoogleCalendarItemId(faker.internet().uuid()),
33+
aAppointmentEventTitle(),
34+
"",
35+
date(),
36+
duration,
37+
null
38+
)
39+
2140
fun aCalendarName(): String = faker.random().randomElementOf(
2241
(1..10).map { faker.internet().emailAddress() } +
2342
listOf(

app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/presets/GoogleCalendarFixturePresets.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package pro.qyoga.tests.fixture.presets
22

3+
import org.springframework.context.ApplicationContext
34
import pro.qyoga.core.calendar.google.GoogleAccount
45
import pro.qyoga.core.calendar.google.GoogleCalendarItem
56
import pro.qyoga.core.users.therapists.TherapistRef
@@ -8,6 +9,8 @@ import pro.qyoga.tests.fixture.object_mothers.calendars.google.GoogleCalendarObj
89
import pro.qyoga.tests.fixture.test_apis.GoogleCalendarTestApi
910
import pro.qyoga.tests.fixture.wiremocks.MockGoogleCalendar
1011
import pro.qyoga.tests.fixture.wiremocks.MockGoogleOAuthServer
12+
import pro.qyoga.tests.infra.wiremock.WireMock
13+
import pro.qyoga.tests.platform.spring.context.getBean
1114

1215

1316
class GoogleCalendarFixturePresets(
@@ -19,7 +22,7 @@ class GoogleCalendarFixturePresets(
1922
fun setupCalendar(
2023
therapistRef: TherapistRef,
2124
calendarId: String,
22-
vararg events: GoogleCalendarItem,
25+
vararg events: GoogleCalendarItem<*>,
2326
shouldBeShown: Boolean = false,
2427
): GoogleAccount {
2528
val refreshToken = "refreshToken"
@@ -34,4 +37,10 @@ class GoogleCalendarFixturePresets(
3437
return account
3538
}
3639

37-
}
40+
}
41+
42+
fun ApplicationContext.googleCalendarFixturePresets() = GoogleCalendarFixturePresets(
43+
MockGoogleOAuthServer(WireMock.wiremock),
44+
MockGoogleCalendar(WireMock.wiremock),
45+
getBean()
46+
)

app/src/testFixtures/kotlin/pro/qyoga/tests/fixture/wiremocks/MockGoogleCalendar.kt

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package pro.qyoga.tests.fixture.wiremocks
22

33
import com.github.tomakehurst.wiremock.WireMockServer
4+
import com.github.tomakehurst.wiremock.client.MappingBuilder
45
import com.github.tomakehurst.wiremock.client.WireMock.*
56
import org.springframework.http.HttpStatus
7+
import org.springframework.web.util.UriUtils
68
import pro.qyoga.core.calendar.google.GoogleCalendar
79
import pro.qyoga.core.calendar.google.GoogleCalendarItem
810
import pro.qyoga.tests.fixture.data.asiaNovosibirskTimeZone
11+
import java.nio.charset.StandardCharsets.UTF_8
12+
import java.time.LocalDateTime
13+
import java.time.ZonedDateTime
914
import java.time.format.DateTimeFormatter
1015

1116
/**
@@ -49,7 +54,7 @@ class MockGoogleCalendar(
4954
private val calendarId: String
5055
) {
5156

52-
fun returnsEvents(vararg events: GoogleCalendarItem) {
57+
fun returnsEvents(vararg events: GoogleCalendarItem<*>) {
5358
wiremockServer.stubFor(
5459
get(
5560
urlPathEqualTo("/google/calendar/v3/calendars/$calendarId/events")
@@ -82,9 +87,11 @@ private fun GoogleCalendar.toJson(): String =
8287
}
8388
"""
8489

85-
private fun GoogleCalendarItem.toJson(): String {
86-
val start = dateTime.atZone(asiaNovosibirskTimeZone).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
87-
val end = endDateTime.atZone(asiaNovosibirskTimeZone).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
90+
private fun GoogleCalendarItem<*>.toJson(): String {
91+
val start = ((dateTime as? ZonedDateTime) ?: (dateTime as LocalDateTime).atZone(asiaNovosibirskTimeZone))
92+
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
93+
val end = ((endDateTime as? ZonedDateTime) ?: (endDateTime as LocalDateTime).atZone(asiaNovosibirskTimeZone))
94+
.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
8895
return """
8996
{
9097
"id": "$id",

0 commit comments

Comments
 (0)