diff --git a/docker-compose.yml b/docker-compose.yml index ea01d3ad..c7f5e1ac 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,6 +42,7 @@ services: JWT_SECRET: d2VhcmVuYXZ5c3dkZXZlbG9wZXJzLmFuZGlhbW1pbmp1bjMwMjE= JWT_EXPIRATION_HOURS: 24 JWT_ISSUER: minjun + QUEUE_SERVER_URL : http://localhost SPRING_PROFILES_ACTIVE: local depends_on: db: diff --git a/docs/open-api.yaml b/docs/open-api.yaml index e327ddbd..2ea00e38 100644 --- a/docs/open-api.yaml +++ b/docs/open-api.yaml @@ -519,10 +519,10 @@ components: pageSize: type: integer format: int32 - paged: - type: boolean unpaged: type: boolean + paged: + type: boolean Reservation: required: - address diff --git a/src/integrationTest/kotlin/com/group4/ticketingservice/Reservation/ReservationTest.kt b/src/integrationTest/kotlin/com/group4/ticketingservice/Reservation/ReservationTest.kt index 8437d325..399425b2 100644 --- a/src/integrationTest/kotlin/com/group4/ticketingservice/Reservation/ReservationTest.kt +++ b/src/integrationTest/kotlin/com/group4/ticketingservice/Reservation/ReservationTest.kt @@ -1,20 +1,26 @@ package com.group4.ticketingservice.Reservation import com.group4.ticketingservice.AbstractIntegrationTest +import com.group4.ticketingservice.dto.QueueResponseDTO import com.group4.ticketingservice.entity.Event import com.group4.ticketingservice.entity.User import com.group4.ticketingservice.repository.EventRepository import com.group4.ticketingservice.repository.ReservationRepository import com.group4.ticketingservice.repository.UserRepository import com.group4.ticketingservice.service.ReservationService +import io.mockk.every +import io.mockk.mockk import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.RepeatedTest import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.HttpMethod +import org.springframework.http.ResponseEntity import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.test.context.TestPropertySource +import org.springframework.web.client.RestTemplate import java.time.Duration.ofHours import java.time.OffsetDateTime import java.util.concurrent.CountDownLatch @@ -50,6 +56,12 @@ class ReservationTest @Autowired constructor( maxAttendees = 100 ) + private val queueSuccess: QueueResponseDTO = QueueResponseDTO( + status = true, + message = "", + data = null + ) + @BeforeEach fun addUserAndEvent() { userRepository.save(sampleUser) eventRepository.save(sampleEvent) @@ -67,6 +79,9 @@ class ReservationTest @Autowired constructor( @RepeatedTest(3) @Test fun `ReservationService_createReservation should not exceed the limit in the concurrency test`() { + val restTemplate: RestTemplate = mockk() + every { restTemplate.exchange(any() as String, HttpMethod.DELETE, null, QueueResponseDTO::class.java) } returns ResponseEntity.ok(queueSuccess) + reservationService.restTemplate = restTemplate val threadCount = 1000 val executorService = Executors.newFixedThreadPool(32) val countDownLatch = CountDownLatch(threadCount) diff --git a/src/integrationTest/resources/application.yml b/src/integrationTest/resources/application.yml index 590b4841..94d6481c 100644 --- a/src/integrationTest/resources/application.yml +++ b/src/integrationTest/resources/application.yml @@ -22,3 +22,7 @@ ticketing: secret: d2VhcmVuYXZ5c3dkZXZlbG9wZXJzLmFuZGlhbW1pbmp1bjMwMjE= expiration-hours: '24' issuer: minjun + + queue: + server: + url: http://localhost:8082/ticket diff --git a/src/main/kotlin/com/group4/ticketingservice/config/Config.kt b/src/main/kotlin/com/group4/ticketingservice/config/Config.kt index 872dc55f..c7695832 100644 --- a/src/main/kotlin/com/group4/ticketingservice/config/Config.kt +++ b/src/main/kotlin/com/group4/ticketingservice/config/Config.kt @@ -3,6 +3,7 @@ package com.group4.ticketingservice.config import org.modelmapper.ModelMapper import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.web.client.RestTemplate @Configuration class Config { @@ -13,4 +14,9 @@ class Config { modelMapper.configuration.isFieldMatchingEnabled = true return modelMapper } + + @Bean + fun restTemplate(): RestTemplate { + return RestTemplate() + } } diff --git a/src/main/kotlin/com/group4/ticketingservice/controller/ReservationController.kt b/src/main/kotlin/com/group4/ticketingservice/controller/ReservationController.kt index 0a14cb19..48841e6e 100644 --- a/src/main/kotlin/com/group4/ticketingservice/controller/ReservationController.kt +++ b/src/main/kotlin/com/group4/ticketingservice/controller/ReservationController.kt @@ -26,107 +26,111 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/reservations") class ReservationController(val reservationService: ReservationService) { - @PostMapping - fun createReservation( - @AuthenticationPrincipal userId: Int, - @RequestBody @Valid - request: ReservationCreateRequest - ): ResponseEntity { - val reservation: Reservation = reservationService.createReservation( - request.eventId!!, - userId, - request.name!!, - request.phoneNumber!!, - request.postCode!!, - request.address!! - ) - val response = ReservationResponse( - id = reservation.id!!, - eventId = reservation.event.id!!, - userId = reservation.user.id!!, - createdAt = reservation.createdAt, - name = reservation.name, - phoneNumber = reservation.phoneNumber, - address = reservation.address, - postCode = reservation.postCode - ) - - val headers = HttpHeaders() - headers.set("Content-Location", "/reservations/%d".format(reservation.id!!)) - - return ResponseEntity(response, headers, HttpStatus.CREATED) - } - - @GetMapping("/{id}") - fun getReservation( - request: HttpServletRequest, - @PathVariable id: Int - ): ResponseEntity { - val reservation = reservationService.getReservation(id) - - val response = ReservationResponse( - id = reservation.id!!, - eventId = reservation.event.id!!, - userId = reservation.user.id!!, - createdAt = reservation.createdAt, - name = reservation.name, - phoneNumber = reservation.phoneNumber, - address = reservation.address, - postCode = reservation.postCode - ) - - val headers = HttpHeaders() - headers.set("Content-Location", request.requestURI) - - return ResponseEntity(response, headers, HttpStatus.OK) - } - - @PutMapping("/{id}") - fun updateReservation( - request: HttpServletRequest, - @PathVariable id: Int, - @RequestBody @Valid - reservationRequest: ReservationUpdateRequest - ): ResponseEntity { - val reservation = reservationService.updateReservation(id, reservationRequest.eventId!!) - - val response = ReservationResponse( - id = reservation.id!!, - eventId = reservation.event.id!!, - userId = reservation.user.id!!, - createdAt = reservation.createdAt, - name = reservation.name, - phoneNumber = reservation.phoneNumber, - address = reservation.address, - postCode = reservation.postCode - ) - - val headers = HttpHeaders() - headers.set("Content-Location", request.requestURI) - - return ResponseEntity(response, headers, HttpStatus.OK) - } - - @DeleteMapping("/{id}") - fun deleteReservation( - @AuthenticationPrincipal userId: Int, - @PathVariable id: Int - ): ResponseEntity { - reservationService.deleteReservation(userId, id) - return ResponseEntity(null, HttpStatus.OK) - } - - @GetMapping - fun getReservations( - request: HttpServletRequest, - @AuthenticationPrincipal userId: Int, - @PageableDefault(size = 10) pageable: Pageable - ): ResponseEntity> { - val page = reservationService.getReservations(userId, pageable) - - val headers = HttpHeaders() - headers.set("Content-Location", request.requestURI) - - return ResponseEntity(page, headers, HttpStatus.OK) + @RestController + @RequestMapping("/reservations") + class ReservationController(val reservationService: ReservationService) { + @PostMapping + fun createReservation( + @AuthenticationPrincipal userId: Int, + @RequestBody @Valid + request: ReservationCreateRequest + ): ResponseEntity { + val reservation: Reservation = reservationService.createReservation( + request.eventId!!, + userId, + request.name!!, + request.phoneNumber!!, + request.postCode!!, + request.address!! + ) + val response = ReservationResponse( + id = reservation.id!!, + eventId = reservation.event.id!!, + userId = reservation.user.id!!, + createdAt = reservation.createdAt, + name = reservation.name, + phoneNumber = reservation.phoneNumber, + address = reservation.address, + postCode = reservation.postCode + ) + + val headers = HttpHeaders() + headers.set("Content-Location", "/reservations/%d".format(reservation.id!!)) + + return ResponseEntity(response, headers, HttpStatus.CREATED) + } + + @GetMapping("/{id}") + fun getReservation( + request: HttpServletRequest, + @PathVariable id: Int + ): ResponseEntity { + val reservation = reservationService.getReservation(id) + + val response = ReservationResponse( + id = reservation.id!!, + eventId = reservation.event.id!!, + userId = reservation.user.id!!, + createdAt = reservation.createdAt, + name = reservation.name, + phoneNumber = reservation.phoneNumber, + address = reservation.address, + postCode = reservation.postCode + ) + + val headers = HttpHeaders() + headers.set("Content-Location", request.requestURI) + + return ResponseEntity(response, headers, HttpStatus.OK) + } + + @PutMapping("/{id}") + fun updateReservation( + request: HttpServletRequest, + @PathVariable id: Int, + @RequestBody @Valid + reservationRequest: ReservationUpdateRequest + ): ResponseEntity { + val reservation = reservationService.updateReservation(id, reservationRequest.eventId!!) + + val response = ReservationResponse( + id = reservation.id!!, + eventId = reservation.event.id!!, + userId = reservation.user.id!!, + createdAt = reservation.createdAt, + name = reservation.name, + phoneNumber = reservation.phoneNumber, + address = reservation.address, + postCode = reservation.postCode + ) + + val headers = HttpHeaders() + headers.set("Content-Location", request.requestURI) + + return ResponseEntity(response, headers, HttpStatus.OK) + } + + @DeleteMapping("/{id}") + fun deleteReservation( + @AuthenticationPrincipal userId: Int, + @PathVariable id: Int + ): ResponseEntity { + reservationService.deleteReservation(userId, id) + return ResponseEntity(null, HttpStatus.OK) + } + + @GetMapping + fun getReservations( + request: HttpServletRequest, + @AuthenticationPrincipal userId: Int, + @PageableDefault(size = 10) pageable: Pageable + ): ResponseEntity> { + val page = reservationService.getReservations(userId, pageable) + + val headers = HttpHeaders() + headers.set("Content-Location", request.requestURI) + + return ResponseEntity(page, headers, HttpStatus.OK) + } } } diff --git a/src/main/kotlin/com/group4/ticketingservice/dto/QueueDTO.kt b/src/main/kotlin/com/group4/ticketingservice/dto/QueueDTO.kt new file mode 100644 index 00000000..b1c60e88 --- /dev/null +++ b/src/main/kotlin/com/group4/ticketingservice/dto/QueueDTO.kt @@ -0,0 +1,18 @@ +package com.group4.ticketingservice.dto + +class QueueResponseDTO( + val status: Boolean = false, + val message: String = "", + val data: TicketInfo? = null +) + +class TicketInfo( + var eventId: String = "", + var userId: String = "", + var isWaiting: Boolean = true, + var offset: Int = 0 +) +data class TicketRequest( + val eventId: Int, + val userId: Int +) diff --git a/src/main/kotlin/com/group4/ticketingservice/service/ReservationService.kt b/src/main/kotlin/com/group4/ticketingservice/service/ReservationService.kt index 16f06030..ec61caab 100644 --- a/src/main/kotlin/com/group4/ticketingservice/service/ReservationService.kt +++ b/src/main/kotlin/com/group4/ticketingservice/service/ReservationService.kt @@ -1,5 +1,6 @@ package com.group4.ticketingservice.service +import com.group4.ticketingservice.dto.QueueResponseDTO import com.group4.ticketingservice.entity.Reservation import com.group4.ticketingservice.repository.EventRepository import com.group4.ticketingservice.repository.ReservationRepository @@ -7,21 +8,35 @@ import com.group4.ticketingservice.repository.UserRepository import com.group4.ticketingservice.utils.exception.CustomException import com.group4.ticketingservice.utils.exception.ErrorCodes import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.data.repository.findByIdOrNull +import org.springframework.http.HttpMethod import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import org.springframework.web.client.HttpClientErrorException +import org.springframework.web.client.RestTemplate import java.time.OffsetDateTime @Service class ReservationService @Autowired constructor( private val userRepository: UserRepository, private val eventRepository: EventRepository, - private val reservationRepository: ReservationRepository + private val reservationRepository: ReservationRepository, + var restTemplate: RestTemplate, + @Value("\${ticketing.queue.server.url}") + private val queueServerURL: String + ) { @Transactional fun createReservation(eventId: Int, userId: Int, name: String, phoneNumber: String, postCode: Int, address: String): Reservation { + try { + restTemplate.exchange("$queueServerURL/running/$eventId/$userId", HttpMethod.DELETE, null, QueueResponseDTO::class.java).body + } catch (e: HttpClientErrorException) { + throw CustomException(ErrorCodes.WAITING_TICKET_NOT_FOUND) + } + val user = userRepository.getReferenceById(userId) val event = eventRepository.findByIdWithPesimisticLock(eventId) ?: throw CustomException(ErrorCodes.ENTITY_NOT_FOUND) diff --git a/src/main/kotlin/com/group4/ticketingservice/utils/exception/ErrorCodes.kt b/src/main/kotlin/com/group4/ticketingservice/utils/exception/ErrorCodes.kt index a5c1d11e..b5dcda06 100644 --- a/src/main/kotlin/com/group4/ticketingservice/utils/exception/ErrorCodes.kt +++ b/src/main/kotlin/com/group4/ticketingservice/utils/exception/ErrorCodes.kt @@ -19,6 +19,7 @@ enum class ErrorCodes(val status: HttpStatus, val message: String, val errorCode // 404 Not Found ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 레코드를 찾을수 없습니다.", 40000), END_POINT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 엔드포인트를 찾을수업습니다.", 40400), + WAITING_TICKET_NOT_FOUND(HttpStatus.NOT_FOUND, "대기열 티켓이 존재하지않아 예약 불가합니다.", 40402), TEST_ERROR(HttpStatus.NOT_FOUND, "테스트 입니다.", 40401), // 409 Conflict diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4f0d52db..31fe43b6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -43,3 +43,8 @@ management: include: prometheus prometheus: enabled: 'true' + +ticketing: + queue: + server: + url: ${QUEUE_SERVER_URL} \ No newline at end of file diff --git a/src/test/kotlin/com/group4/ticketingservice/reservation/ReservationServiceTest.kt b/src/test/kotlin/com/group4/ticketingservice/reservation/ReservationServiceTest.kt index 01fb6670..0c81fd8a 100644 --- a/src/test/kotlin/com/group4/ticketingservice/reservation/ReservationServiceTest.kt +++ b/src/test/kotlin/com/group4/ticketingservice/reservation/ReservationServiceTest.kt @@ -1,5 +1,6 @@ package com.group4.ticketingservice.reservation +import com.group4.ticketingservice.dto.QueueResponseDTO import com.group4.ticketingservice.entity.Event import com.group4.ticketingservice.entity.Reservation import com.group4.ticketingservice.entity.User @@ -14,6 +15,11 @@ import io.mockk.mockk import io.mockk.verify import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import org.springframework.http.HttpMethod +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.client.HttpClientErrorException +import org.springframework.web.client.RestTemplate import java.time.Duration.ofHours import java.time.OffsetDateTime import java.util.Optional @@ -22,12 +28,15 @@ class ReservationServiceTest() { private val userRepository: UserRepository = mockk() private val eventRepository: EventRepository = mockk() private val reservationRepository: ReservationRepository = mockk() + private val restTemplate: RestTemplate = mockk() + private val reservationService: ReservationService = ReservationService( userRepository = userRepository, eventRepository = eventRepository, - reservationRepository = reservationRepository + reservationRepository = reservationRepository, + queueServerURL = "url", + restTemplate = restTemplate ) - val sampleUserId = 1 val sampleUser = User( @@ -62,13 +71,18 @@ class ReservationServiceTest() { user = sampleUser, event = sampleEvent ) + private val queueSuccess: QueueResponseDTO = QueueResponseDTO( + status = true, + message = "", + data = null + ) @Test fun `ReservationService_createReservation invoke ReservationRepository_save`() { every { userRepository.getReferenceById(any()) } returns sampleUser every { eventRepository.findByIdWithPesimisticLock(any()) } returns sampleEvent every { eventRepository.findByIdWithOptimisicLock(any()) } returns sampleEvent - + every { restTemplate.exchange(any() as String, HttpMethod.DELETE, null, QueueResponseDTO::class.java) } returns ResponseEntity.ok(queueSuccess) every { eventRepository.saveAndFlush(any()) } returns sampleEvent every { reservationRepository.saveAndFlush(any()) } returns sampleReservation reservationService.createReservation(1, 1, "김해군", "010-1234-5678", 1, "서울") @@ -79,7 +93,18 @@ class ReservationServiceTest() { fun `ReservationService_createReservation throw custom exception when time not allowed`() { every { userRepository.getReferenceById(any()) } returns sampleUser every { eventRepository.findByIdWithPesimisticLock(any()) } returns sampleWrongEvent + every { restTemplate.exchange(any() as String, HttpMethod.DELETE, null, QueueResponseDTO::class.java) } returns ResponseEntity.ok(queueSuccess) + every { eventRepository.saveAndFlush(any()) } returns sampleEvent + every { reservationRepository.saveAndFlush(any()) } returns sampleReservation + assertThrows { reservationService.createReservation(1, 1, "김해군", "010-1234-5678", 1, "서울") } + } + + @Test + fun `ReservationService_createReservation throw custom exception when doesn't have waiting ticket`() { + every { userRepository.getReferenceById(any()) } returns sampleUser + every { eventRepository.findByIdWithPesimisticLock(any()) } returns sampleWrongEvent + every { restTemplate.exchange(any() as String, HttpMethod.DELETE, null, QueueResponseDTO::class.java) } throws HttpClientErrorException(HttpStatus.NOT_FOUND) every { eventRepository.saveAndFlush(any()) } returns sampleEvent every { reservationRepository.saveAndFlush(any()) } returns sampleReservation diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 45b29919..689d2b63 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -14,3 +14,6 @@ ticketing: secret: d2VhcmVuYXZ5c3dkZXZlbG9wZXJzLmFuZGlhbW1pbmp1bjMwMjE= expiration-hours: '24' issuer: minjun + queue: + server: + url: http://localhost:8082/ticket \ No newline at end of file