diff --git a/build.gradle.kts b/build.gradle.kts index aabe020..a4be4bd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,48 +1,50 @@ plugins { - kotlin("jvm") version "1.9.25" - kotlin("plugin.spring") version "1.9.25" - id("org.springframework.boot") version "3.4.1" - id("io.spring.dependency-management") version "1.1.7" + kotlin("jvm") version "1.9.25" + kotlin("plugin.spring") version "1.9.25" + id("org.springframework.boot") version "3.4.1" + id("io.spring.dependency-management") version "1.1.7" } group = "ru.vyatsu" version = "1.0.0" java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } } repositories { - mavenLocal() - mavenCentral() + mavenLocal() + mavenCentral() } dependencies { - implementation("org.springframework.boot:spring-boot-starter-data-jdbc") - implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server") - implementation("org.springframework.boot:spring-boot-starter-security") - implementation("org.springframework.boot:spring-boot-starter-web") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin") - implementation("org.jetbrains.kotlin:kotlin-reflect") - implementation("org.liquibase:liquibase-core") - implementation("ru.vyatsu:qr-access-hardware-contract:1.0.0") - runtimeOnly("org.postgresql:postgresql") - testImplementation("org.springframework.boot:spring-boot-starter-test") - testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") - testImplementation("org.springframework.security:spring-security-test") - testImplementation("org.testcontainers:postgresql") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") - implementation("org.yaml:snakeyaml") + implementation("org.springframework.boot:spring-boot-starter-data-jdbc") + implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + compileOnly("org.projectlombok:lombok") + annotationProcessor("org.projectlombok:lombok") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.liquibase:liquibase-core") + implementation("ru.vyatsu:qr-access-hardware-contract:1.0.0") + runtimeOnly("org.postgresql:postgresql") + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + testImplementation("org.springframework.security:spring-security-test") + testImplementation("org.testcontainers:postgresql") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + implementation("org.yaml:snakeyaml") } kotlin { - compilerOptions { - freeCompilerArgs.addAll("-Xjsr305=strict") - } + compilerOptions { + freeCompilerArgs.addAll("-Xjsr305=strict") + } } tasks.withType { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/config/SecurityConfig.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/config/SecurityConfig.kt index 2262816..7773a62 100644 --- a/src/main/kotlin/ru/vyatsu/qr_access_api/config/SecurityConfig.kt +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/config/SecurityConfig.kt @@ -13,7 +13,10 @@ class SecurityConfig { @Bean fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain { - return http.authorizeHttpRequests { it.anyRequest().authenticated() } + return http.authorizeHttpRequests { + it.requestMatchers("/public/**").permitAll() + .anyRequest().authenticated() + } .oauth2ResourceServer { it.jwt(Customizer.withDefaults()) } .build() } diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/slots/controller/PublicSlotsController.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/controller/PublicSlotsController.kt new file mode 100644 index 0000000..aefb010 --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/controller/PublicSlotsController.kt @@ -0,0 +1,18 @@ +package ru.vyatsu.qr_access_api.slots.controller + +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import ru.vyatsu.qr_access_api.slots.response.SlotResponse +import ru.vyatsu.qr_access_api.slots.service.SlotService + +@RestController +@RequestMapping("public") +class PublicSlotsController(val service: SlotService) { + + @GetMapping("/slots/{partnerId}") + fun getSlots(@PathVariable partnerId: String): SlotResponse { + return service.getAllSlotsByPartner(partnerId) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/slots/repository/SlotRepository.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/repository/SlotRepository.kt new file mode 100644 index 0000000..b184c70 --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/repository/SlotRepository.kt @@ -0,0 +1,81 @@ +package ru.vyatsu.qr_access_api.slots.repository + +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.stereotype.Repository +import ru.vyatsu.qr_access_api.slots.repository.entity.* + +const val FIND_DOORS_SCHEDULE_BY_PARTNER_ID = + "select d.id, d.description, d.count, s.date, s.start_time, s.end_time from oauth2_registered_client u join doors d on (d.unit_id = u.client_id) join schedule s on (d.id = s.door_id) where u.partner_id = ?" +const val FIND_RENT_DOORS_BY_DOOR_IDS = + "select d.id, r.date, r.start_time, r.end_time from oauth2_registered_client u join doors d on (d.unit_id = u.client_id) join rent r on (r.door_id = d.id) where u.partner_id = ? order by r.start_time asc" + +@Repository +class SlotRepository(private val jdbc: JdbcTemplate) { + + fun findAllDoorsWithScheduleByPartnerId(partnerId: String): Collection { + val doors: MutableMap = mutableMapOf() + jdbc.query({ + val stmt = it.prepareStatement(FIND_DOORS_SCHEDULE_BY_PARTNER_ID) + stmt.setString(1, partnerId) + stmt + }, { rs -> + val id = rs.getString("id") + doors[id]?.also { + it.schedule.add( + ScheduleEntry( + rs.getTime("start_time").toLocalTime(), + rs.getTime("end_time").toLocalTime(), + rs.getDate("date").toLocalDate() + ) + ) + } ?: run { + doors[id] = DoorWithSchedule( + id, rs.getString("description"), rs.getInt("count"), mutableListOf( + ScheduleEntry( + rs.getTime("start_time").toLocalTime(), + rs.getTime("end_time").toLocalTime(), + rs.getDate("date").toLocalDate() + ) + ) + ) + } + }) + return doors.values + } + + fun findRentDateTimesByDoorsId(partnerId: String): DoorRent { + val doors = DoorRent(mutableMapOf()) + jdbc.query({ + val stmt = it.prepareStatement(FIND_RENT_DOORS_BY_DOOR_IDS) + stmt.setString(1, partnerId) + stmt + }, { rs -> + val id = rs.getString("id") + val date = rs.getDate("date").toLocalDate() + doors.dates[id]?.also { d -> + d.rentDate[date]?.also { times -> + times.add(RentTime(rs.getTime("start_time").toLocalTime(), rs.getTime("end_time").toLocalTime())) + } ?: run { + d.rentDate[date] = mutableListOf( + RentTime( + rs.getTime("start_time").toLocalTime(), + rs.getTime("end_time").toLocalTime() + ) + ) + } + } ?: run { + doors.dates[id] = RentDate( + mutableMapOf( + date to mutableListOf( + RentTime( + rs.getTime("start_time").toLocalTime(), + rs.getTime("end_time").toLocalTime() + ) + ) + ) + ) + } + }) + return doors + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/slots/repository/entity/DoorRent.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/repository/entity/DoorRent.kt new file mode 100644 index 0000000..e94ef9e --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/repository/entity/DoorRent.kt @@ -0,0 +1,3 @@ +package ru.vyatsu.qr_access_api.slots.repository.entity + +data class DoorRent(val dates: MutableMap) \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/slots/repository/entity/DoorWithSchedule.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/repository/entity/DoorWithSchedule.kt new file mode 100644 index 0000000..61aa0a3 --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/repository/entity/DoorWithSchedule.kt @@ -0,0 +1,8 @@ +package ru.vyatsu.qr_access_api.slots.repository.entity + +data class DoorWithSchedule( + val id: String, + val description: String, + val count: Int, + val schedule: MutableList +) \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/slots/repository/entity/RentDate.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/repository/entity/RentDate.kt new file mode 100644 index 0000000..c1d1af6 --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/repository/entity/RentDate.kt @@ -0,0 +1,5 @@ +package ru.vyatsu.qr_access_api.slots.repository.entity + +import java.time.LocalDate + +data class RentDate(val rentDate: MutableMap>) \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/slots/repository/entity/RentTime.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/repository/entity/RentTime.kt new file mode 100644 index 0000000..fcc46ba --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/repository/entity/RentTime.kt @@ -0,0 +1,5 @@ +package ru.vyatsu.qr_access_api.slots.repository.entity + +import java.time.LocalTime + +data class RentTime(val startTime: LocalTime, val endTime: LocalTime) \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/slots/repository/entity/ScheduleEntry.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/repository/entity/ScheduleEntry.kt new file mode 100644 index 0000000..2a8ed50 --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/repository/entity/ScheduleEntry.kt @@ -0,0 +1,6 @@ +package ru.vyatsu.qr_access_api.slots.repository.entity + +import java.time.LocalDate +import java.time.LocalTime + +data class ScheduleEntry(val startTime: LocalTime, val endTime: LocalTime, val date: LocalDate) \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/slots/response/Door.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/response/Door.kt new file mode 100644 index 0000000..a1fbe97 --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/response/Door.kt @@ -0,0 +1,3 @@ +package ru.vyatsu.qr_access_api.slots.response + +data class Door(val id: String, val description: String, val slots: List) \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/slots/response/Slot.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/response/Slot.kt new file mode 100644 index 0000000..cf0ca9b --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/response/Slot.kt @@ -0,0 +1,6 @@ +package ru.vyatsu.qr_access_api.slots.response + +import java.time.LocalDate +import java.time.LocalTime + +data class Slot(val startTime: LocalTime, val endTime: LocalTime, val date: LocalDate, val status: SlotStatus) \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/slots/response/SlotResponse.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/response/SlotResponse.kt new file mode 100644 index 0000000..65ce707 --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/response/SlotResponse.kt @@ -0,0 +1,3 @@ +package ru.vyatsu.qr_access_api.slots.response + +data class SlotResponse(val doors: List) \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/slots/response/SlotStatus.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/response/SlotStatus.kt new file mode 100644 index 0000000..12dc4f6 --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/response/SlotStatus.kt @@ -0,0 +1,5 @@ +package ru.vyatsu.qr_access_api.slots.response + +enum class SlotStatus { + FREE, BOOKED, OUT_OF_WORKING_TIME +} \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/slots/service/SlotService.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/service/SlotService.kt new file mode 100644 index 0000000..e1af191 --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/service/SlotService.kt @@ -0,0 +1,51 @@ +package ru.vyatsu.qr_access_api.slots.service + +import org.springframework.stereotype.Service +import ru.vyatsu.qr_access_api.slots.repository.SlotRepository +import ru.vyatsu.qr_access_api.slots.response.Door +import ru.vyatsu.qr_access_api.slots.response.Slot +import ru.vyatsu.qr_access_api.slots.response.SlotResponse +import ru.vyatsu.qr_access_api.slots.response.SlotStatus +import java.time.LocalDate +import java.time.LocalTime + +@Service +class SlotService(private val repository: SlotRepository) { + fun getAllSlotsByPartner(partnerId: String): SlotResponse { + // TODO: Учитывать количество мест и переписать логику + val doorsWithSchedule = repository.findAllDoorsWithScheduleByPartnerId(partnerId) + val doorRents = repository.findRentDateTimesByDoorsId(partnerId) + val doors: MutableList = mutableListOf() + doorsWithSchedule.forEach { d -> + val slots: MutableList = mutableListOf() + d.schedule.forEach { sch -> + val rentTimes = doorRents.dates[d.id]?.rentDate?.get(sch.date) ?: listOf() + createIntermediateSlot(LocalTime.MIN, sch.startTime, sch.date, SlotStatus.OUT_OF_WORKING_TIME) + ?.also { slots.add(it) } + rentTimes.forEach { rt -> + val lastSlot: Slot? = if (slots.lastIndex == -1) null else slots.last() + createIntermediateSlot(lastSlot?.endTime, rt.startTime, sch.date, SlotStatus.FREE) + ?.also { slots.add(it) } + slots.add(Slot(rt.startTime, rt.endTime, sch.date, SlotStatus.BOOKED)) + } + var lastSlot: Slot? = if (slots.lastIndex == -1) null else slots.last() + createIntermediateSlot(lastSlot?.endTime, sch.endTime, sch.date, SlotStatus.FREE) + ?.also { slots.add(it) } + lastSlot = if (slots.lastIndex == -1) null else slots.last() + createIntermediateSlot(lastSlot?.endTime, LocalTime.MAX, sch.date, SlotStatus.OUT_OF_WORKING_TIME) + ?.also { slots.add(it) } + } + doors.add(Door(d.id, d.description, slots)) + } + return SlotResponse(doors) + } + + private fun createIntermediateSlot( + startTime: LocalTime?, + endTime: LocalTime, + date: LocalDate, + status: SlotStatus + ): Slot? { + return if (startTime != null && startTime != endTime) Slot(startTime, endTime, date, status) else null + } +} \ No newline at end of file