From 46bf29a5eb78a185217d8e0b73da8cf507ce86c7 Mon Sep 17 00:00:00 2001 From: kashiuno Date: Tue, 18 Mar 2025 18:29:36 +0300 Subject: [PATCH] Bank gateway integration, rest for getting slots, email sending. --- build.gradle.kts | 2 + .../qr_access_api/QrAccessApiApplication.kt | 4 + .../booking/controller/BookingController.kt | 24 ++++ .../job/CleanExpiredNotPayedRentJob.kt | 18 +++ .../booking/repository/BookingRepository.kt | 106 ++++++++++++++++++ .../repository/entity/RentWithEmail.kt | 3 + .../booking/request/BookCallbackRequest.kt | 3 + .../booking/request/BookRequest.kt | 10 ++ .../booking/request/BookResponse.kt | 3 + .../booking/service/BookingService.kt | 37 ++++++ .../ru/vyatsu/qr_access_api/common/Log.kt | 8 ++ .../common/config/EmailConfig.kt | 27 +++++ .../common/exception/ValidationException.kt | 4 + .../qr_access_api/config/SecurityConfig.kt | 22 +++- .../email/job/EmailOutboxSendJob.kt | 71 ++++++++++++ .../email/repository/EmailRepository.kt | 54 +++++++++ .../email/repository/entity/EmailOutbox.kt | 5 + .../{ => qr}/controller/QrSyncController.kt | 4 +- .../{ => qr}/controller/QrUsedController.kt | 2 +- .../{ => qr}/repository/QrRepository.kt | 2 +- .../{ => qr}/service/QrSyncService.kt | 4 +- ...cSlotsController.kt => SlotsController.kt} | 7 +- .../slots/repository/SlotRepository.kt | 9 +- .../slots/service/SlotService.kt | 2 +- .../db/changelog/1.0.0/changelog.yml | 8 +- .../repository/RepositoryTest.kt | 1 + 26 files changed, 420 insertions(+), 20 deletions(-) create mode 100644 src/main/kotlin/ru/vyatsu/qr_access_api/booking/controller/BookingController.kt create mode 100644 src/main/kotlin/ru/vyatsu/qr_access_api/booking/job/CleanExpiredNotPayedRentJob.kt create mode 100644 src/main/kotlin/ru/vyatsu/qr_access_api/booking/repository/BookingRepository.kt create mode 100644 src/main/kotlin/ru/vyatsu/qr_access_api/booking/repository/entity/RentWithEmail.kt create mode 100644 src/main/kotlin/ru/vyatsu/qr_access_api/booking/request/BookCallbackRequest.kt create mode 100644 src/main/kotlin/ru/vyatsu/qr_access_api/booking/request/BookRequest.kt create mode 100644 src/main/kotlin/ru/vyatsu/qr_access_api/booking/request/BookResponse.kt create mode 100644 src/main/kotlin/ru/vyatsu/qr_access_api/booking/service/BookingService.kt create mode 100644 src/main/kotlin/ru/vyatsu/qr_access_api/common/Log.kt create mode 100644 src/main/kotlin/ru/vyatsu/qr_access_api/common/config/EmailConfig.kt create mode 100644 src/main/kotlin/ru/vyatsu/qr_access_api/common/exception/ValidationException.kt create mode 100644 src/main/kotlin/ru/vyatsu/qr_access_api/email/job/EmailOutboxSendJob.kt create mode 100644 src/main/kotlin/ru/vyatsu/qr_access_api/email/repository/EmailRepository.kt create mode 100644 src/main/kotlin/ru/vyatsu/qr_access_api/email/repository/entity/EmailOutbox.kt rename src/main/kotlin/ru/vyatsu/qr_access_api/{ => qr}/controller/QrSyncController.kt (79%) rename src/main/kotlin/ru/vyatsu/qr_access_api/{ => qr}/controller/QrUsedController.kt (89%) rename src/main/kotlin/ru/vyatsu/qr_access_api/{ => qr}/repository/QrRepository.kt (96%) rename src/main/kotlin/ru/vyatsu/qr_access_api/{ => qr}/service/QrSyncService.kt (79%) rename src/main/kotlin/ru/vyatsu/qr_access_api/slots/controller/{PublicSlotsController.kt => SlotsController.kt} (55%) diff --git a/build.gradle.kts b/build.gradle.kts index a4be4bd..3d524b5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,7 +24,9 @@ dependencies { 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("org.springframework.boot:spring-boot-starter-mail") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("io.nayuki:qrcodegen:1.8.0") compileOnly("org.projectlombok:lombok") annotationProcessor("org.projectlombok:lombok") implementation("org.jetbrains.kotlin:kotlin-reflect") diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/QrAccessApiApplication.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/QrAccessApiApplication.kt index 9e91e3f..8830898 100644 --- a/src/main/kotlin/ru/vyatsu/qr_access_api/QrAccessApiApplication.kt +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/QrAccessApiApplication.kt @@ -2,8 +2,12 @@ package ru.vyatsu.qr_access_api import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.scheduling.annotation.EnableScheduling + +//TODO: Сделать ретраи везде, где в блок-схемах они указаны @SpringBootApplication +@EnableScheduling class QrAccessApiApplication fun main(args: Array) { diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/booking/controller/BookingController.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/booking/controller/BookingController.kt new file mode 100644 index 0000000..c8d69ba --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/booking/controller/BookingController.kt @@ -0,0 +1,24 @@ +package ru.vyatsu.qr_access_api.booking.controller + +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController +import ru.vyatsu.qr_access_api.booking.request.BookCallbackRequest +import ru.vyatsu.qr_access_api.booking.service.BookingService +import ru.vyatsu.qr_access_api.booking.request.BookRequest +import ru.vyatsu.qr_access_api.booking.request.BookResponse + +@RestController +class BookingController(private val service: BookingService) { + // TODO: Убрать /public, так как эти методы должны быть закрыты авторизацией client_credential и корсами + // TODO: Для общего процесса бронирования нужно сделать альтернативный путь для шлюза с опросом. + @PostMapping("/public/book") + fun book(@RequestBody request: BookRequest): BookResponse { + return BookResponse(service.book(request)) + } + + @PostMapping("/public/book/callback") + fun bookCallback(@RequestBody request: BookCallbackRequest) { + return service.bookPayed(request.rentId) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/booking/job/CleanExpiredNotPayedRentJob.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/booking/job/CleanExpiredNotPayedRentJob.kt new file mode 100644 index 0000000..61de069 --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/booking/job/CleanExpiredNotPayedRentJob.kt @@ -0,0 +1,18 @@ +package ru.vyatsu.qr_access_api.booking.job + +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service +import ru.vyatsu.qr_access_api.booking.repository.BookingRepository +import ru.vyatsu.qr_access_api.common.logger +import java.time.LocalDateTime + +@Service +class CleanExpiredNotPayedRentJob(private val repository: BookingRepository) { + + @Scheduled(cron = "0 */5 * * * *") + fun clean() { + val deletedCount = + repository.deleteRentByDateCreatedLessThen(LocalDateTime.now().minusMinutes(10)) + logger().info("CleanExpiredNotPayedRentJob deleted {} rents", deletedCount) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/booking/repository/BookingRepository.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/booking/repository/BookingRepository.kt new file mode 100644 index 0000000..7f49356 --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/booking/repository/BookingRepository.kt @@ -0,0 +1,106 @@ +package ru.vyatsu.qr_access_api.booking.repository + +import org.springframework.dao.EmptyResultDataAccessException +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.jdbc.support.GeneratedKeyHolder +import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional +import ru.vyatsu.apis.NotFoundException +import ru.vyatsu.qr_access_api.booking.repository.entity.RentWithEmail +import java.sql.Date +import java.sql.Statement +import java.sql.Time +import java.sql.Timestamp +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.util.* + +private const val ADD_BOOKING_INFO_QUERY = + "insert into rents(id, start_time, end_time, client_id, date, door_id, qr_code, payed, date_created) values (?, ? ,?, ?, ?, ?, ?, false, CURRENT_TIMESTAMP) RETURNING id" + +private const val CREATE_NEW_CLIENT = + "insert into clients(id, email, email_is_confirmed) values (?, ?, false) RETURNING id" + +private const val FIND_CLIENT_BY_EMAIL = "select id from clients where email = ?" + +private const val MARK_RENT_AS_PAYED_QUERY = "update rents set payed=true where id = ?" + +@Repository +class BookingRepository(val template: JdbcTemplate) { + + @Transactional + fun book(doorId: String, date: LocalDate, startTime: LocalTime, endTime: LocalTime, clientEmail: String): String { + val clientIdHolder = GeneratedKeyHolder() + val rentIdHolder = GeneratedKeyHolder() + var clientId: String? = null + try { + clientId = template.queryForObject( + FIND_CLIENT_BY_EMAIL, + { rs, _ -> rs.getString("id") }, + clientEmail + ) + } catch (_: EmptyResultDataAccessException) { + } + + if (clientId == null) { + template.update({ + val stmt = it.prepareStatement(CREATE_NEW_CLIENT, Statement.RETURN_GENERATED_KEYS) + stmt.setString(1, UUID.randomUUID().toString()) + stmt.setString(2, clientEmail) + stmt + }, clientIdHolder) + clientId = clientIdHolder.getKeyAs(String::class.java) + } + + if (clientId == null) throw RuntimeException("clientId is null even after insert, booking cannot be continued") + + val insertedRows = template.update({ + val stmt = it.prepareStatement(ADD_BOOKING_INFO_QUERY, Statement.RETURN_GENERATED_KEYS) + stmt.setString(1, UUID.randomUUID().toString()) + stmt.setTime(2, Time.valueOf(startTime)) + stmt.setTime(3, Time.valueOf(endTime)) + stmt.setString(4, clientId) + stmt.setDate(5, Date.valueOf(date)) + stmt.setString(6, doorId) + stmt.setString(7, UUID.randomUUID().toString()) + stmt + }, rentIdHolder) + + val insertedRentId = rentIdHolder.getKeyAs(String::class.java) + if (insertedRows <= 0 || insertedRentId == null) throw RuntimeException( + "Inserted rows number is invalid: %d. Have to be more then 0".format( + insertedRows + ) + ) + + return insertedRentId + } + + fun findRentWithClientById(id: String): RentWithEmail { + return template.queryForObject( + "select r.qr_code, c.email from rents r join clients c on (c.id = r.client_id) where r.id = ?", + { rs, _ -> RentWithEmail(rs.getString("qr_code"), rs.getString("email")) }, + id + ) ?: throw NotFoundException("Cannot find rent with id %s".format(id)) + } + + fun markBookingPayed(rentId: String) { + val updatedAmount = template.update { + val stmt = it.prepareStatement(MARK_RENT_AS_PAYED_QUERY) + stmt.setString(1, rentId) + stmt + } + + if (updatedAmount == 0) + throw NotFoundException("Cannot find rent with id %s".format(rentId)) + } + + fun deleteRentByDateCreatedLessThen(dateTime: LocalDateTime): Int { + return template.update { + val stmt = it.prepareStatement("delete from rents where date_created <= ? and payed = false") + stmt.setTimestamp(1, Timestamp.valueOf(dateTime)) + stmt + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/booking/repository/entity/RentWithEmail.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/booking/repository/entity/RentWithEmail.kt new file mode 100644 index 0000000..862d638 --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/booking/repository/entity/RentWithEmail.kt @@ -0,0 +1,3 @@ +package ru.vyatsu.qr_access_api.booking.repository.entity + +data class RentWithEmail(val qrCode: String, val email: String) \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/booking/request/BookCallbackRequest.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/booking/request/BookCallbackRequest.kt new file mode 100644 index 0000000..5d383ca --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/booking/request/BookCallbackRequest.kt @@ -0,0 +1,3 @@ +package ru.vyatsu.qr_access_api.booking.request + +data class BookCallbackRequest(val rentId: String) diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/booking/request/BookRequest.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/booking/request/BookRequest.kt new file mode 100644 index 0000000..2752a4f --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/booking/request/BookRequest.kt @@ -0,0 +1,10 @@ +package ru.vyatsu.qr_access_api.booking.request + +import java.time.LocalDateTime + +data class BookRequest( + val startDateTime: LocalDateTime, + val endDateTime: LocalDateTime, + val doorId: String, + val clientEmail: String +) \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/booking/request/BookResponse.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/booking/request/BookResponse.kt new file mode 100644 index 0000000..6dc9a58 --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/booking/request/BookResponse.kt @@ -0,0 +1,3 @@ +package ru.vyatsu.qr_access_api.booking.request + +data class BookResponse(val rentId: String) diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/booking/service/BookingService.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/booking/service/BookingService.kt new file mode 100644 index 0000000..f9347a1 --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/booking/service/BookingService.kt @@ -0,0 +1,37 @@ +package ru.vyatsu.qr_access_api.booking.service + +import com.fasterxml.jackson.databind.node.JsonNodeFactory +import com.fasterxml.jackson.databind.node.ObjectNode +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import ru.vyatsu.qr_access_api.booking.repository.BookingRepository +import ru.vyatsu.qr_access_api.booking.request.BookRequest +import ru.vyatsu.qr_access_api.common.exception.ValidationException +import ru.vyatsu.qr_access_api.email.repository.EmailRepository + +@Service +class BookingService(val bookingRepository: BookingRepository, val emailRepository: EmailRepository) { + fun book(request: BookRequest): String { + // TODO: Произвести еще валидации, если нужны + val date = request.startDateTime.toLocalDate() + if (date != request.endDateTime.toLocalDate()) { + throw ValidationException( + "startDateTime, endDateTime", + "startDateTime and endDateTime have to have the same day" + ) + } + val startTime = request.startDateTime.toLocalTime() + val endTime = request.endDateTime.toLocalTime() + + return bookingRepository.book(request.doorId, date, startTime, endTime, request.clientEmail) + } + + @Transactional + fun bookPayed(bookId: String) { + bookingRepository.markBookingPayed(bookId) + val (qrCode, email) = bookingRepository.findRentWithClientById(bookId) + val additionalData = JsonNodeFactory.instance.objectNode() + additionalData.set("qr_code", JsonNodeFactory.instance.textNode(qrCode)) + emailRepository.createEmailOutboxRecord(email, "qr_code", additionalData) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/common/Log.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/common/Log.kt new file mode 100644 index 0000000..8a692fa --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/common/Log.kt @@ -0,0 +1,8 @@ +package ru.vyatsu.qr_access_api.common + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +inline fun T.logger(): Logger { + return LoggerFactory.getLogger(T::class.java) +} \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/common/config/EmailConfig.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/common/config/EmailConfig.kt new file mode 100644 index 0000000..2956b7a --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/common/config/EmailConfig.kt @@ -0,0 +1,27 @@ +package ru.vyatsu.qr_access_api.common.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.mail.javamail.JavaMailSender +import org.springframework.mail.javamail.JavaMailSenderImpl + + +@Configuration +class EmailConfig { + @Bean + fun mailSender(): JavaMailSender { + val mailSender = JavaMailSenderImpl() + mailSender.host = "smtp.yandex.ru" + mailSender.port = 587 + mailSender.username = "kashiuno@yandex.ru" + mailSender.password = "qmpEMP262049!!!?EEWChaosMeteor" + + val props = mailSender.javaMailProperties + props["mail.transport.protocol"] = "smtp" + props["mail.smtp.auth"] = "true" + props["mail.smtp.starttls.enable"] = "true" + props["mail.debug"] = "true" + + return mailSender + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/common/exception/ValidationException.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/common/exception/ValidationException.kt new file mode 100644 index 0000000..f54cce9 --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/common/exception/ValidationException.kt @@ -0,0 +1,4 @@ +package ru.vyatsu.qr_access_api.common.exception + +class ValidationException(val fieldName: String, override val message: String) : + RuntimeException("fieldName: %s -- message: %s".format(fieldName, message)) \ No newline at end of file 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 7773a62..014cb3d 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 @@ -2,22 +2,40 @@ package ru.vyatsu.qr_access_api.config import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod import org.springframework.security.config.Customizer import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.web.SecurityFilterChain +import org.springframework.web.cors.CorsConfiguration @Configuration -@EnableWebSecurity +@EnableWebSecurity(debug = true) class SecurityConfig { @Bean fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain { return http.authorizeHttpRequests { - it.requestMatchers("/public/**").permitAll() + it.requestMatchers("/public/**", "/error").permitAll() .anyRequest().authenticated() } .oauth2ResourceServer { it.jwt(Customizer.withDefaults()) } + .cors { c -> + c.configurationSource { + val config = CorsConfiguration() + config.addAllowedOrigin("http://localhost:3000") + config.allowedMethods = listOf( + HttpMethod.GET.name(), + HttpMethod.POST.name() + ) + config.allowedHeaders = listOf( + HttpHeaders.CONTENT_TYPE + ) + config + } + } + .csrf { c -> c.disable() } .build() } } \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/email/job/EmailOutboxSendJob.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/email/job/EmailOutboxSendJob.kt new file mode 100644 index 0000000..0c9fa8c --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/email/job/EmailOutboxSendJob.kt @@ -0,0 +1,71 @@ +package ru.vyatsu.qr_access_api.email.job + +import io.nayuki.qrcodegen.QrCode +import org.springframework.mail.javamail.JavaMailSender +import org.springframework.mail.javamail.MimeMessageHelper +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service +import ru.vyatsu.qr_access_api.common.logger +import ru.vyatsu.qr_access_api.email.repository.EmailRepository +import java.awt.image.BufferedImage +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.util.* +import javax.imageio.ImageIO + + +@Service +class EmailOutboxSendJob(val repository: EmailRepository, val mailSender: JavaMailSender) { + + @Scheduled(cron = "*/5 * * * * *") + fun sendEmails() { + val messagesToSend = repository.findRecordsToSendWithBlock() + + messagesToSend.forEach { + val msg = mailSender.createMimeMessage() + + val helper = MimeMessageHelper(msg, true) + + helper.setFrom("kashiuno@yandex.ru") + helper.setTo(it.email) + helper.setSubject("Приобретение qr-кода") + helper.setText("Вы приобрели проход в коворкинг на нашем сайте. qr-код во вложении. QR-код нужно приложить к сканеру соответствующей двери и она откроется") + + val qrCode = it.additionalData.get("qr_code") + val code = QrCode.encodeText(qrCode.asText(), QrCode.Ecc.MEDIUM) + val image = toImage(code, 3, 2) + val os = ByteArrayOutputStream() + ImageIO.write(image, "png", os) + val imageIS = ByteArrayInputStream(os.toByteArray()) + helper.addAttachment("qr_code.png") { imageIS } + } + + val recordsWasDeletedCount = repository.deleteRecordsToSend(messagesToSend.map { it.id }) + logger().info("Emails sent {}", recordsWasDeletedCount) + } + + companion object { + private fun toImage(qr: QrCode, scale: Int, border: Int, lightColor: Int, darkColor: Int): BufferedImage { + Objects.requireNonNull(qr) + require(!(scale <= 0 || border < 0)) { "Value out of range" } + require(!(border > Int.MAX_VALUE / 2 || qr.size + border * 2L > Int.MAX_VALUE / scale)) { "Scale or border too large" } + + val result = BufferedImage( + (qr.size + border * 2) * scale, + (qr.size + border * 2) * scale, + BufferedImage.TYPE_INT_RGB + ) + for (y in 0 until result.height) { + for (x in 0 until result.width) { + val color = qr.getModule(x / scale - border, y / scale - border) + result.setRGB(x, y, if (color) darkColor else lightColor) + } + } + return result + } + + fun toImage(qr: QrCode?, scale: Int, border: Int): BufferedImage { + return toImage(qr!!, scale, border, 0xFFFFFF, 0x000000) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/email/repository/EmailRepository.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/email/repository/EmailRepository.kt new file mode 100644 index 0000000..8cc6e17 --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/email/repository/EmailRepository.kt @@ -0,0 +1,54 @@ +package ru.vyatsu.qr_access_api.email.repository + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.jdbc.core.BatchPreparedStatementSetter +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.stereotype.Repository +import ru.vyatsu.qr_access_api.email.repository.entity.EmailOutbox +import java.sql.PreparedStatement +import java.util.* + +private const val INSERT_EMAIL_OUTBOX_RECORD = + "insert into email_outbox(id, email, template, additional_info) values (?, ?, ?, (to_json(?::json)))" + +@Repository +class EmailRepository(private val template: JdbcTemplate, private val om: ObjectMapper) { + fun createEmailOutboxRecord(email: String, msgTemplate: String, additionalInfo: JsonNode) { + val insertedCount = template.update { + val stmt = it.prepareStatement(INSERT_EMAIL_OUTBOX_RECORD) + stmt.setString(1, UUID.randomUUID().toString()) + stmt.setString(2, email) + stmt.setString(3, msgTemplate) + + stmt.setObject(4, om.writeValueAsString(additionalInfo)) + stmt + } + + if (insertedCount != 1) + throw RuntimeException("Inserted rows should be equals to 1") + } + + fun findRecordsToSendWithBlock(): List { + return template.query("select id, email, template, additional_info from email_outbox for update", { rs, _ -> + EmailOutbox( + rs.getString("id"), + rs.getString("email"), + rs.getString("template"), + om.readTree(rs.getString("additional_info")) + ) + }) + } + + fun deleteRecordsToSend(ids: List): Int { + val query = "delete from email_outbox where id = ?" + return template.batchUpdate(query, object : BatchPreparedStatementSetter { + override fun setValues(ps: PreparedStatement, i: Int) { + ps.setString(1, ids[i]) + } + + override fun getBatchSize(): Int = ids.size + }) + .sum() + } +} \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/email/repository/entity/EmailOutbox.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/email/repository/entity/EmailOutbox.kt new file mode 100644 index 0000000..b518b27 --- /dev/null +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/email/repository/entity/EmailOutbox.kt @@ -0,0 +1,5 @@ +package ru.vyatsu.qr_access_api.email.repository.entity + +import com.fasterxml.jackson.databind.JsonNode + +data class EmailOutbox(val id: String, val email: String, val template: String, val additionalData: JsonNode) \ No newline at end of file diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/controller/QrSyncController.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/qr/controller/QrSyncController.kt similarity index 79% rename from src/main/kotlin/ru/vyatsu/qr_access_api/controller/QrSyncController.kt rename to src/main/kotlin/ru/vyatsu/qr_access_api/qr/controller/QrSyncController.kt index e14467b..2d782f1 100644 --- a/src/main/kotlin/ru/vyatsu/qr_access_api/controller/QrSyncController.kt +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/qr/controller/QrSyncController.kt @@ -1,10 +1,10 @@ -package ru.vyatsu.qr_access_api.controller +package ru.vyatsu.qr_access_api.qr.controller import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.RestController import ru.vyatsu.apis.QrApi import ru.vyatsu.models.QrCodesResponse -import ru.vyatsu.qr_access_api.service.QrSyncService +import ru.vyatsu.qr_access_api.qr.service.QrSyncService @RestController class QrSyncController(val syncService: QrSyncService) : QrApi { diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/controller/QrUsedController.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/qr/controller/QrUsedController.kt similarity index 89% rename from src/main/kotlin/ru/vyatsu/qr_access_api/controller/QrUsedController.kt rename to src/main/kotlin/ru/vyatsu/qr_access_api/qr/controller/QrUsedController.kt index d3548af..ba00564 100644 --- a/src/main/kotlin/ru/vyatsu/qr_access_api/controller/QrUsedController.kt +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/qr/controller/QrUsedController.kt @@ -1,4 +1,4 @@ -package ru.vyatsu.qr_access_api.controller +package ru.vyatsu.qr_access_api.qr.controller import org.springframework.http.HttpStatusCode import org.springframework.http.ResponseEntity diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/repository/QrRepository.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/qr/repository/QrRepository.kt similarity index 96% rename from src/main/kotlin/ru/vyatsu/qr_access_api/repository/QrRepository.kt rename to src/main/kotlin/ru/vyatsu/qr_access_api/qr/repository/QrRepository.kt index 5d69c87..162cb67 100644 --- a/src/main/kotlin/ru/vyatsu/qr_access_api/repository/QrRepository.kt +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/qr/repository/QrRepository.kt @@ -1,4 +1,4 @@ -package ru.vyatsu.qr_access_api.repository +package ru.vyatsu.qr_access_api.qr.repository import org.springframework.jdbc.core.JdbcTemplate import org.springframework.stereotype.Repository diff --git a/src/main/kotlin/ru/vyatsu/qr_access_api/service/QrSyncService.kt b/src/main/kotlin/ru/vyatsu/qr_access_api/qr/service/QrSyncService.kt similarity index 79% rename from src/main/kotlin/ru/vyatsu/qr_access_api/service/QrSyncService.kt rename to src/main/kotlin/ru/vyatsu/qr_access_api/qr/service/QrSyncService.kt index cae1fc6..d20390f 100644 --- a/src/main/kotlin/ru/vyatsu/qr_access_api/service/QrSyncService.kt +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/qr/service/QrSyncService.kt @@ -1,9 +1,9 @@ -package ru.vyatsu.qr_access_api.service +package ru.vyatsu.qr_access_api.qr.service import org.springframework.security.core.context.SecurityContextHolder import org.springframework.stereotype.Service import ru.vyatsu.models.QrCode -import ru.vyatsu.qr_access_api.repository.QrRepository +import ru.vyatsu.qr_access_api.qr.repository.QrRepository @Service class QrSyncService(val qrRepository: QrRepository) { 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/SlotsController.kt similarity index 55% rename from src/main/kotlin/ru/vyatsu/qr_access_api/slots/controller/PublicSlotsController.kt rename to src/main/kotlin/ru/vyatsu/qr_access_api/slots/controller/SlotsController.kt index aefb010..251ffbc 100644 --- a/src/main/kotlin/ru/vyatsu/qr_access_api/slots/controller/PublicSlotsController.kt +++ b/src/main/kotlin/ru/vyatsu/qr_access_api/slots/controller/SlotsController.kt @@ -1,15 +1,12 @@ 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 org.springframework.web.bind.annotation.* 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) { +class SlotsController(val service: SlotService) { @GetMapping("/slots/{partnerId}") fun getSlots(@PathVariable partnerId: String): SlotResponse { 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 index b184c70..73d9e55 100644 --- 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 @@ -6,12 +6,11 @@ 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" +const val FIND_RENT_DOORS_BY_PARTNER_ID = + "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 rents 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({ @@ -43,10 +42,10 @@ class SlotRepository(private val jdbc: JdbcTemplate) { return doors.values } - fun findRentDateTimesByDoorsId(partnerId: String): DoorRent { + fun findRentDateTimesByPartnerId(partnerId: String): DoorRent { val doors = DoorRent(mutableMapOf()) jdbc.query({ - val stmt = it.prepareStatement(FIND_RENT_DOORS_BY_DOOR_IDS) + val stmt = it.prepareStatement(FIND_RENT_DOORS_BY_PARTNER_ID) stmt.setString(1, partnerId) stmt }, { rs -> 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 index e1af191..498d1b1 100644 --- 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 @@ -14,7 +14,7 @@ class SlotService(private val repository: SlotRepository) { fun getAllSlotsByPartner(partnerId: String): SlotResponse { // TODO: Учитывать количество мест и переписать логику val doorsWithSchedule = repository.findAllDoorsWithScheduleByPartnerId(partnerId) - val doorRents = repository.findRentDateTimesByDoorsId(partnerId) + val doorRents = repository.findRentDateTimesByPartnerId(partnerId) val doors: MutableList = mutableListOf() doorsWithSchedule.forEach { d -> val slots: MutableList = mutableListOf() diff --git a/src/main/resources/db/changelog/1.0.0/changelog.yml b/src/main/resources/db/changelog/1.0.0/changelog.yml index 859bd40..35d90c5 100644 --- a/src/main/resources/db/changelog/1.0.0/changelog.yml +++ b/src/main/resources/db/changelog/1.0.0/changelog.yml @@ -132,6 +132,11 @@ databaseChangeLog: type: TEXT constraints: nullable: true + - column: + name: price + type: DECIMAL(12, 2) + constraints: + nullable: false - createTable: tableName: clients columns: @@ -145,6 +150,7 @@ databaseChangeLog: - column: constraints: nullable: false + unique: true name: email type: TEXT - column: @@ -153,7 +159,7 @@ databaseChangeLog: name: email_is_confirmed type: BOOLEAN - createTable: - tableName: rent + tableName: rents columns: - column: constraints: diff --git a/src/test/kotlin/ru/vyatsu/qr_access_api/repository/RepositoryTest.kt b/src/test/kotlin/ru/vyatsu/qr_access_api/repository/RepositoryTest.kt index 1a7a698..c3b6b15 100644 --- a/src/test/kotlin/ru/vyatsu/qr_access_api/repository/RepositoryTest.kt +++ b/src/test/kotlin/ru/vyatsu/qr_access_api/repository/RepositoryTest.kt @@ -7,6 +7,7 @@ import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Import import org.springframework.jdbc.core.JdbcTemplate import ru.vyatsu.qr_access_api.database.utils.InsertDatabaseHelper +import ru.vyatsu.qr_access_api.qr.repository.QrRepository @JdbcTest @Import(RepositoryTest.Configuration::class)