generated from pricelees/issue-pr-template
<!-- 제목 양식 --> <!-- [이슈번호] 작업 요약 (예시: [#10] Gitea 템플릿 생성) --> ## 📝 관련 이슈 및 PR **PR과 관련된 이슈 번호** - #30 ## ✨ 작업 내용 <!-- 어떤 작업을 했는지 알려주세요! --> - ReservationService를 읽기(Find) / 쓰기(Write) 서비스로 분리 - 모든 도메인에 repository를 사용하는 Finder, Writer, Validator 도입 -> ReservationService에 있는 조회, 검증, 쓰기 작업을 별도의 클래스로 분리하기 위함이었고, 이 과정에서 다른 도메인에도 도입함. ## 🧪 테스트 <!-- 어떤 테스트를 생각했고 진행했는지 알려주세요! --> 새로 추가된 기능 & 클래스는 모두 테스트 추가하였고, 작업 후 전체 테스트 완료 ## 📚 참고 자료 및 기타 <!-- 참고한 자료, 또는 논의할 사항이 있다면 알려주세요! --> Reviewed-on: #31 Co-authored-by: pricelees <priceelees@gmail.com> Co-committed-by: pricelees <priceelees@gmail.com>
This commit is contained in:
parent
d3e22888ed
commit
5fe1427fc1
@ -3,64 +3,56 @@ package roomescape.auth.business
|
|||||||
import io.github.oshai.kotlinlogging.KLogger
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import roomescape.auth.exception.AuthErrorCode
|
import roomescape.auth.exception.AuthErrorCode
|
||||||
import roomescape.auth.exception.AuthException
|
import roomescape.auth.exception.AuthException
|
||||||
import roomescape.auth.infrastructure.jwt.JwtHandler
|
import roomescape.auth.infrastructure.jwt.JwtHandler
|
||||||
import roomescape.auth.web.LoginCheckResponse
|
import roomescape.auth.web.LoginCheckResponse
|
||||||
import roomescape.auth.web.LoginRequest
|
import roomescape.auth.web.LoginRequest
|
||||||
import roomescape.auth.web.LoginResponse
|
import roomescape.auth.web.LoginResponse
|
||||||
import roomescape.common.exception.RoomescapeException
|
import roomescape.member.implement.MemberFinder
|
||||||
import roomescape.member.business.MemberService
|
|
||||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||||
|
|
||||||
private val log: KLogger = KotlinLogging.logger {}
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class AuthService(
|
class AuthService(
|
||||||
private val memberService: MemberService,
|
private val memberFinder: MemberFinder,
|
||||||
private val jwtHandler: JwtHandler,
|
private val jwtHandler: JwtHandler,
|
||||||
) {
|
) {
|
||||||
|
@Transactional(readOnly = true)
|
||||||
fun login(request: LoginRequest): LoginResponse {
|
fun login(request: LoginRequest): LoginResponse {
|
||||||
val params = "email=${request.email}, password=${request.password}"
|
val params = "email=${request.email}, password=${request.password}"
|
||||||
log.debug { "[AuthService.login] 로그인 시작: $params" }
|
log.debug { "[AuthService.login] 시작: $params" }
|
||||||
|
|
||||||
val member: MemberEntity = fetchMemberOrThrow(AuthErrorCode.LOGIN_FAILED, params, "login") {
|
val member: MemberEntity = fetchOrThrow(AuthErrorCode.LOGIN_FAILED) {
|
||||||
memberService.findByEmailAndPassword(request.email, request.password)
|
memberFinder.findByEmailAndPassword(request.email, request.password)
|
||||||
}
|
}
|
||||||
|
|
||||||
val accessToken: String = jwtHandler.createToken(member.id!!)
|
val accessToken: String = jwtHandler.createToken(member.id!!)
|
||||||
|
|
||||||
return LoginResponse(accessToken)
|
return LoginResponse(accessToken)
|
||||||
.also { log.info { "[AuthService.login] 로그인 완료: memberId=${member.id}" } }
|
.also { log.info { "[AuthService.login] 완료: email=${request.email}, memberId=${member.id}" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
fun checkLogin(memberId: Long): LoginCheckResponse {
|
fun checkLogin(memberId: Long): LoginCheckResponse {
|
||||||
log.debug { "[AuthService.checkLogin] 로그인 확인 시작: memberId=$memberId" }
|
log.debug { "[AuthService.checkLogin] 시작: memberId=$memberId" }
|
||||||
val member: MemberEntity =
|
|
||||||
fetchMemberOrThrow(AuthErrorCode.MEMBER_NOT_FOUND, "memberId=$memberId", "checkLogin") {
|
val member: MemberEntity = fetchOrThrow(AuthErrorCode.MEMBER_NOT_FOUND) { memberFinder.findById(memberId) }
|
||||||
memberService.findById(memberId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return LoginCheckResponse(member.name, member.role.name)
|
return LoginCheckResponse(member.name, member.role.name)
|
||||||
.also { log.info { "[AuthService.checkLogin] 로그인 확인 완료: memberId=$memberId" } }
|
.also { log.info { "[AuthService.checkLogin] 완료: memberId=$memberId, role=${it.role}" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchOrThrow(errorCode: AuthErrorCode, block: () -> MemberEntity): MemberEntity {
|
||||||
|
try {
|
||||||
|
return block()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw AuthException(errorCode, e.message ?: errorCode.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun logout(memberId: Long) {
|
fun logout(memberId: Long) {
|
||||||
log.info { "[AuthService.logout] 로그아웃: memberId=$memberId" }
|
log.info { "[AuthService.logout] 로그아웃: memberId=$memberId" }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchMemberOrThrow(
|
|
||||||
errorCode: AuthErrorCode,
|
|
||||||
params: String,
|
|
||||||
calledBy: String,
|
|
||||||
block: () -> MemberEntity,
|
|
||||||
): MemberEntity {
|
|
||||||
try {
|
|
||||||
return block()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e !is RoomescapeException) {
|
|
||||||
log.warn(e) { "[AuthService.$calledBy] 회원 조회 실패: $params" }
|
|
||||||
}
|
|
||||||
throw AuthException(errorCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
package roomescape.auth.infrastructure.jwt
|
package roomescape.auth.infrastructure.jwt
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.jsonwebtoken.ExpiredJwtException
|
import io.jsonwebtoken.ExpiredJwtException
|
||||||
import io.jsonwebtoken.Jwts
|
import io.jsonwebtoken.Jwts
|
||||||
import io.jsonwebtoken.security.Keys
|
import io.jsonwebtoken.security.Keys
|
||||||
@ -10,6 +12,8 @@ import roomescape.auth.exception.AuthException
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.crypto.SecretKey
|
import javax.crypto.SecretKey
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class JwtHandler(
|
class JwtHandler(
|
||||||
@Value("\${security.jwt.token.secret-key}")
|
@Value("\${security.jwt.token.secret-key}")
|
||||||
@ -21,6 +25,7 @@ class JwtHandler(
|
|||||||
private val secretKey: SecretKey = Keys.hmacShaKeyFor(secretKeyString.toByteArray())
|
private val secretKey: SecretKey = Keys.hmacShaKeyFor(secretKeyString.toByteArray())
|
||||||
|
|
||||||
fun createToken(memberId: Long): String {
|
fun createToken(memberId: Long): String {
|
||||||
|
log.debug { "[JwtHandler.createToken] 시작: memberId=$memberId" }
|
||||||
val date = Date()
|
val date = Date()
|
||||||
val accessTokenExpiredAt = Date(date.time + (tokenTtlSeconds * 1_000))
|
val accessTokenExpiredAt = Date(date.time + (tokenTtlSeconds * 1_000))
|
||||||
|
|
||||||
@ -30,10 +35,12 @@ class JwtHandler(
|
|||||||
.expiration(accessTokenExpiredAt)
|
.expiration(accessTokenExpiredAt)
|
||||||
.signWith(secretKey)
|
.signWith(secretKey)
|
||||||
.compact()
|
.compact()
|
||||||
|
.also { log.debug { "[JwtHandler.createToken] 완료. memberId=$memberId, token=$it" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMemberIdFromToken(token: String?): Long {
|
fun getMemberIdFromToken(token: String?): Long {
|
||||||
try {
|
try {
|
||||||
|
log.debug { "[JwtHandler.getMemberIdFromToken] 시작: token=$token" }
|
||||||
return Jwts.parser()
|
return Jwts.parser()
|
||||||
.verifyWith(secretKey)
|
.verifyWith(secretKey)
|
||||||
.build()
|
.build()
|
||||||
@ -41,6 +48,7 @@ class JwtHandler(
|
|||||||
.payload
|
.payload
|
||||||
.get(MEMBER_ID_CLAIM_KEY, Number::class.java)
|
.get(MEMBER_ID_CLAIM_KEY, Number::class.java)
|
||||||
.toLong()
|
.toLong()
|
||||||
|
.also { log.debug { "[JwtHandler.getMemberIdFromToken] 완료. memberId=$it, token=$token" } }
|
||||||
} catch (_: IllegalArgumentException) {
|
} catch (_: IllegalArgumentException) {
|
||||||
throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND)
|
throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND)
|
||||||
} catch (_: ExpiredJwtException) {
|
} catch (_: ExpiredJwtException) {
|
||||||
|
|||||||
@ -7,8 +7,8 @@ import org.springframework.web.bind.annotation.GetMapping
|
|||||||
import org.springframework.web.bind.annotation.PostMapping
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
import org.springframework.web.bind.annotation.RequestBody
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
import roomescape.auth.docs.AuthAPI
|
|
||||||
import roomescape.auth.business.AuthService
|
import roomescape.auth.business.AuthService
|
||||||
|
import roomescape.auth.docs.AuthAPI
|
||||||
import roomescape.auth.web.support.MemberId
|
import roomescape.auth.web.support.MemberId
|
||||||
import roomescape.common.dto.response.CommonApiResponse
|
import roomescape.common.dto.response.CommonApiResponse
|
||||||
|
|
||||||
|
|||||||
@ -11,14 +11,14 @@ import org.springframework.web.servlet.HandlerInterceptor
|
|||||||
import roomescape.auth.exception.AuthErrorCode
|
import roomescape.auth.exception.AuthErrorCode
|
||||||
import roomescape.auth.exception.AuthException
|
import roomescape.auth.exception.AuthException
|
||||||
import roomescape.auth.infrastructure.jwt.JwtHandler
|
import roomescape.auth.infrastructure.jwt.JwtHandler
|
||||||
import roomescape.member.business.MemberService
|
import roomescape.member.implement.MemberFinder
|
||||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||||
|
|
||||||
private val log: KLogger = KotlinLogging.logger {}
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class AuthInterceptor(
|
class AuthInterceptor(
|
||||||
private val memberService: MemberService,
|
private val memberFinder: MemberFinder,
|
||||||
private val jwtHandler: JwtHandler
|
private val jwtHandler: JwtHandler
|
||||||
) : HandlerInterceptor {
|
) : HandlerInterceptor {
|
||||||
override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
|
override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
|
||||||
@ -50,10 +50,10 @@ class AuthInterceptor(
|
|||||||
private fun findMember(accessToken: String?): MemberEntity {
|
private fun findMember(accessToken: String?): MemberEntity {
|
||||||
try {
|
try {
|
||||||
val memberId = jwtHandler.getMemberIdFromToken(accessToken)
|
val memberId = jwtHandler.getMemberIdFromToken(accessToken)
|
||||||
return memberService.findById(memberId)
|
return memberFinder.findById(memberId)
|
||||||
.also { MDC.put("member_id", "$memberId") }
|
.also { MDC.put("member_id", "$memberId") }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.info { "[AuthInterceptor] 회원 조회 실패. accessToken = ${accessToken}" }
|
log.info { "[AuthInterceptor] 회원 조회 실패. accessToken = $accessToken" }
|
||||||
val errorCode = AuthErrorCode.MEMBER_NOT_FOUND
|
val errorCode = AuthErrorCode.MEMBER_NOT_FOUND
|
||||||
throw AuthException(errorCode, e.message ?: errorCode.message)
|
throw AuthException(errorCode, e.message ?: errorCode.message)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,72 +1,44 @@
|
|||||||
package roomescape.member.business
|
package roomescape.member.business
|
||||||
|
|
||||||
import com.github.f4b6a3.tsid.TsidFactory
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import roomescape.common.config.next
|
import roomescape.member.implement.MemberFinder
|
||||||
import roomescape.member.exception.MemberErrorCode
|
import roomescape.member.implement.MemberWriter
|
||||||
import roomescape.member.exception.MemberException
|
|
||||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||||
import roomescape.member.infrastructure.persistence.MemberRepository
|
|
||||||
import roomescape.member.infrastructure.persistence.Role
|
import roomescape.member.infrastructure.persistence.Role
|
||||||
import roomescape.member.web.*
|
import roomescape.member.web.*
|
||||||
|
|
||||||
private val log = KotlinLogging.logger {}
|
private val log = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Transactional(readOnly = true)
|
|
||||||
class MemberService(
|
class MemberService(
|
||||||
private val tsidFactory: TsidFactory,
|
private val memberWriter: MemberWriter,
|
||||||
private val memberRepository: MemberRepository,
|
private val memberFinder: MemberFinder,
|
||||||
) {
|
) {
|
||||||
|
@Transactional(readOnly = true)
|
||||||
fun findMembers(): MemberRetrieveListResponse {
|
fun findMembers(): MemberRetrieveListResponse {
|
||||||
log.debug { "[MemberService.findMembers] 회원 조회 시작" }
|
log.debug { "[MemberService.findMembers] 시작" }
|
||||||
|
|
||||||
return memberRepository.findAll()
|
return memberFinder.findAll()
|
||||||
.also { log.info { "[MemberService.findMembers] 회원 ${it.size}명 조회 완료" } }
|
|
||||||
.toRetrieveListResponse()
|
.toRetrieveListResponse()
|
||||||
|
.also { log.info { "[MemberService.findMembers] 완료. ${it.members.size}명 반환" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
fun findById(memberId: Long): MemberEntity {
|
fun findById(memberId: Long): MemberEntity {
|
||||||
return fetchOrThrow("findById", "memberId=$memberId") {
|
log.debug { "[MemberService.findById] 시작" }
|
||||||
memberRepository.findByIdOrNull(memberId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun findByEmailAndPassword(email: String, password: String): MemberEntity {
|
return memberFinder.findById(memberId)
|
||||||
return fetchOrThrow("findByEmailAndPassword", "email=$email, password=$password") {
|
.also { log.info { "[MemberService.findById] 완료. memberId=${memberId}, email=${it.email}" } }
|
||||||
memberRepository.findByEmailAndPassword(email, password)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun createMember(request: SignupRequest): SignupResponse {
|
fun createMember(request: SignupRequest): SignupResponse {
|
||||||
memberRepository.findByEmail(request.email)?.let {
|
log.debug { "[MemberService.createMember] 시작" }
|
||||||
log.info { "[MemberService.createMember] 회원가입 실패(이메일 중복): email=${request.email}" }
|
|
||||||
throw MemberException(MemberErrorCode.DUPLICATE_EMAIL)
|
|
||||||
}
|
|
||||||
|
|
||||||
val member = MemberEntity(
|
return memberWriter.create(request.name, request.email, request.password, Role.MEMBER)
|
||||||
_id = tsidFactory.next(),
|
.toSignupResponse()
|
||||||
name = request.name,
|
.also { log.info { "[MemberService.create] 완료: email=${request.email} memberId=${it.id}" } }
|
||||||
email = request.email,
|
|
||||||
password = request.password,
|
|
||||||
role = Role.MEMBER
|
|
||||||
)
|
|
||||||
|
|
||||||
return memberRepository.save(member).toSignupResponse()
|
|
||||||
.also { log.info { "[MemberService.create] 회원가입 완료: email=${request.email} memberId=${it.id}" } }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchOrThrow(calledBy: String, params: String, block: () -> MemberEntity?): MemberEntity {
|
|
||||||
log.debug { "[MemberService.$calledBy] 회원 조회 시작: $params" }
|
|
||||||
return block()
|
|
||||||
?.also { log.info { "[MemberService.$calledBy] 회원 조회 완료: memberId=${it.id}" } }
|
|
||||||
?: run {
|
|
||||||
log.info { "[MemberService.$calledBy] 회원 조회 실패: $params" }
|
|
||||||
throw MemberException(MemberErrorCode.MEMBER_NOT_FOUND)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
src/main/kotlin/roomescape/member/implement/MemberFinder.kt
Normal file
47
src/main/kotlin/roomescape/member/implement/MemberFinder.kt
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package roomescape.member.implement
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import roomescape.member.exception.MemberErrorCode
|
||||||
|
import roomescape.member.exception.MemberException
|
||||||
|
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||||
|
import roomescape.member.infrastructure.persistence.MemberRepository
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class MemberFinder(
|
||||||
|
private val memberRepository: MemberRepository
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun findAll(): List<MemberEntity> {
|
||||||
|
log.debug { "[MemberFinder.findAll] 회원 조회 시작" }
|
||||||
|
|
||||||
|
return memberRepository.findAll()
|
||||||
|
.also { log.debug { "[MemberFinder.findAll] 회원 ${it.size}명 조회 완료" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findById(id: Long): MemberEntity {
|
||||||
|
log.debug { "[MemberFinder.findById] 조회 시작: memberId=$id" }
|
||||||
|
|
||||||
|
return memberRepository.findByIdOrNull(id)
|
||||||
|
?.also { log.debug { "[MemberFinder.findById] 조회 완료: memberId=$id, email=${it.email}" } }
|
||||||
|
?: run {
|
||||||
|
log.info { "[MemberFinder.findById] 조회 실패: id=$id" }
|
||||||
|
throw MemberException(MemberErrorCode.MEMBER_NOT_FOUND)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findByEmailAndPassword(email: String, password: String): MemberEntity {
|
||||||
|
log.debug { "[MemberFinder.findByEmailAndPassword] 조회 시작: email=$email, password=$password" }
|
||||||
|
|
||||||
|
return memberRepository.findByEmailAndPassword(email, password)
|
||||||
|
?.also { log.debug { "[MemberFinder.findByEmailAndPassword] 조회 완료: email=${email}, memberId=${it.id}" } }
|
||||||
|
?: run {
|
||||||
|
log.info { "[MemberFinder.findByEmailAndPassword] 조회 실패: email=${email}, password=${password}" }
|
||||||
|
throw MemberException(MemberErrorCode.MEMBER_NOT_FOUND)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
package roomescape.member.implement
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import roomescape.member.exception.MemberErrorCode
|
||||||
|
import roomescape.member.exception.MemberException
|
||||||
|
import roomescape.member.infrastructure.persistence.MemberRepository
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class MemberValidator(
|
||||||
|
private val memberRepository: MemberRepository
|
||||||
|
) {
|
||||||
|
fun validateCanSignup(email: String) {
|
||||||
|
log.debug { "[MemberValidator.validateCanSignup] 시작: email=$email" }
|
||||||
|
|
||||||
|
if (memberRepository.existsByEmail(email)) {
|
||||||
|
log.info { "[MemberValidator.validateCanSignup] 중복 이메일: email=$email" }
|
||||||
|
throw MemberException(MemberErrorCode.DUPLICATE_EMAIL)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug { "[MemberValidator.validateCanSignup] 완료: email=$email" }
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/main/kotlin/roomescape/member/implement/MemberWriter.kt
Normal file
35
src/main/kotlin/roomescape/member/implement/MemberWriter.kt
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package roomescape.member.implement
|
||||||
|
|
||||||
|
import com.github.f4b6a3.tsid.TsidFactory
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import roomescape.common.config.next
|
||||||
|
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||||
|
import roomescape.member.infrastructure.persistence.MemberRepository
|
||||||
|
import roomescape.member.infrastructure.persistence.Role
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class MemberWriter(
|
||||||
|
private val tsidFactory: TsidFactory,
|
||||||
|
private val memberValidator: MemberValidator,
|
||||||
|
private val memberRepository: MemberRepository
|
||||||
|
) {
|
||||||
|
fun create(name: String, email: String, password: String, role: Role): MemberEntity {
|
||||||
|
log.debug { "[MemberWriter.create] 시작: email=$email" }
|
||||||
|
memberValidator.validateCanSignup(email)
|
||||||
|
|
||||||
|
val member = MemberEntity(
|
||||||
|
_id = tsidFactory.next(),
|
||||||
|
name = name,
|
||||||
|
email = email,
|
||||||
|
password = password,
|
||||||
|
role = role
|
||||||
|
)
|
||||||
|
|
||||||
|
return memberRepository.save(member)
|
||||||
|
.also { log.debug { "[MemberWriter.create] 완료: email=$email, memberId=${it.id}" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@ package roomescape.member.infrastructure.persistence
|
|||||||
import org.springframework.data.jpa.repository.JpaRepository
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
|
||||||
interface MemberRepository : JpaRepository<MemberEntity, Long> {
|
interface MemberRepository : JpaRepository<MemberEntity, Long> {
|
||||||
fun findByEmailAndPassword(email: String, password: String): MemberEntity?
|
fun existsByEmail(email: String): Boolean
|
||||||
|
|
||||||
fun findByEmail(email: String): MemberEntity?
|
fun findByEmailAndPassword(email: String, password: String): MemberEntity?
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,13 @@
|
|||||||
package roomescape.payment.business
|
package roomescape.payment.business
|
||||||
|
|
||||||
import com.github.f4b6a3.tsid.TsidFactory
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import roomescape.common.config.next
|
import roomescape.payment.implement.PaymentFinder
|
||||||
import roomescape.payment.exception.PaymentErrorCode
|
import roomescape.payment.implement.PaymentWriter
|
||||||
import roomescape.payment.exception.PaymentException
|
|
||||||
import roomescape.payment.infrastructure.client.PaymentApproveResponse
|
import roomescape.payment.infrastructure.client.PaymentApproveResponse
|
||||||
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity
|
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity
|
||||||
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository
|
|
||||||
import roomescape.payment.infrastructure.persistence.PaymentEntity
|
import roomescape.payment.infrastructure.persistence.PaymentEntity
|
||||||
import roomescape.payment.infrastructure.persistence.PaymentRepository
|
|
||||||
import roomescape.payment.web.PaymentCancelRequest
|
import roomescape.payment.web.PaymentCancelRequest
|
||||||
import roomescape.payment.web.PaymentCancelResponse
|
import roomescape.payment.web.PaymentCancelResponse
|
||||||
import roomescape.payment.web.PaymentCreateResponse
|
import roomescape.payment.web.PaymentCreateResponse
|
||||||
@ -23,108 +19,70 @@ private val log = KotlinLogging.logger {}
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
class PaymentService(
|
class PaymentService(
|
||||||
private val tsidFactory: TsidFactory,
|
private val paymentFinder: PaymentFinder,
|
||||||
private val paymentRepository: PaymentRepository,
|
private val paymentWriter: PaymentWriter
|
||||||
private val canceledPaymentRepository: CanceledPaymentRepository,
|
|
||||||
) {
|
) {
|
||||||
@Transactional
|
@Transactional(readOnly = true)
|
||||||
fun createPayment(
|
fun existsByReservationId(reservationId: Long): Boolean {
|
||||||
approveResponse: PaymentApproveResponse,
|
log.debug { "[PaymentService.existsByReservationId] 시작: reservationId=$reservationId" }
|
||||||
reservation: ReservationEntity,
|
|
||||||
): PaymentCreateResponse {
|
|
||||||
log.debug { "[PaymentService.createPayment] 결제 정보 저장 시작: request=$approveResponse, reservationId=${reservation.id}" }
|
|
||||||
val payment = PaymentEntity(
|
|
||||||
_id = tsidFactory.next(),
|
|
||||||
orderId = approveResponse.orderId,
|
|
||||||
paymentKey = approveResponse.paymentKey,
|
|
||||||
totalAmount = approveResponse.totalAmount,
|
|
||||||
reservation = reservation,
|
|
||||||
approvedAt = approveResponse.approvedAt
|
|
||||||
)
|
|
||||||
|
|
||||||
return paymentRepository.save(payment)
|
return paymentFinder.existsPaymentByReservationId(reservationId)
|
||||||
.toCreateResponse()
|
.also { log.info { "[PaymentService.existsByReservationId] 완료: reservationId=$reservationId, isPaid=$it" } }
|
||||||
.also { log.info { "[PaymentService.createPayment] 결제 정보 저장 완료: paymentId=${it.id}, reservationId=${reservation.id}" } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional
|
||||||
fun isReservationPaid(reservationId: Long): Boolean {
|
fun createPayment(
|
||||||
log.debug { "[PaymentService.isReservationPaid] 예약 결제 여부 확인 시작: reservationId=$reservationId" }
|
approvedPaymentInfo: PaymentApproveResponse,
|
||||||
|
reservation: ReservationEntity,
|
||||||
|
): PaymentCreateResponse {
|
||||||
|
log.debug { "[PaymentService.createPayment] 시작: paymentKey=${approvedPaymentInfo.paymentKey}, reservationId=${reservation.id}" }
|
||||||
|
|
||||||
return paymentRepository.existsByReservationId(reservationId)
|
val created: PaymentEntity = paymentWriter.create(
|
||||||
.also { log.info { "[PaymentService.isReservationPaid] 예약 결제 여부 확인 완료: reservationId=$reservationId, isPaid=$it" } }
|
paymentKey = approvedPaymentInfo.paymentKey,
|
||||||
|
orderId = approvedPaymentInfo.orderId,
|
||||||
|
totalAmount = approvedPaymentInfo.totalAmount,
|
||||||
|
approvedAt = approvedPaymentInfo.approvedAt,
|
||||||
|
reservation = reservation
|
||||||
|
)
|
||||||
|
|
||||||
|
return created.toCreateResponse()
|
||||||
|
.also { log.info { "[PaymentService.createPayment] 완료: paymentKey=${it.paymentKey}, reservationId=${reservation.id}, paymentId=${it.id}" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun createCanceledPayment(
|
fun createCanceledPayment(
|
||||||
cancelInfo: PaymentCancelResponse,
|
canceledPaymentInfo: PaymentCancelResponse,
|
||||||
approvedAt: OffsetDateTime,
|
approvedAt: OffsetDateTime,
|
||||||
paymentKey: String,
|
paymentKey: String,
|
||||||
): CanceledPaymentEntity {
|
): CanceledPaymentEntity {
|
||||||
log.debug {
|
log.debug { "[PaymentService.createCanceledPayment] 시작: paymentKey=$paymentKey" }
|
||||||
"[PaymentService.createCanceledPayment] 결제 취소 정보 저장 시작: paymentKey=$paymentKey" +
|
|
||||||
", cancelInfo=$cancelInfo"
|
val created: CanceledPaymentEntity = paymentWriter.createCanceled(
|
||||||
}
|
cancelReason = canceledPaymentInfo.cancelReason,
|
||||||
val canceledPayment = CanceledPaymentEntity(
|
cancelAmount = canceledPaymentInfo.cancelAmount,
|
||||||
_id = tsidFactory.next(),
|
canceledAt = canceledPaymentInfo.canceledAt,
|
||||||
paymentKey = paymentKey,
|
|
||||||
cancelReason = cancelInfo.cancelReason,
|
|
||||||
cancelAmount = cancelInfo.cancelAmount,
|
|
||||||
approvedAt = approvedAt,
|
approvedAt = approvedAt,
|
||||||
canceledAt = cancelInfo.canceledAt
|
paymentKey = paymentKey
|
||||||
)
|
)
|
||||||
|
|
||||||
return canceledPaymentRepository.save(canceledPayment)
|
return created.also {
|
||||||
.also {
|
log.info { "[PaymentService.createCanceledPayment] 완료: paymentKey=${paymentKey}, canceledPaymentId=${it.id}" }
|
||||||
log.info {
|
}
|
||||||
"[PaymentService.createCanceledPayment] 결제 취소 정보 생성 완료: canceledPaymentId=${it.id}" +
|
|
||||||
", paymentKey=${paymentKey}, amount=${cancelInfo.cancelAmount}, canceledAt=${it.canceledAt}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun createCanceledPaymentByReservationId(reservationId: Long): PaymentCancelRequest {
|
fun createCanceledPayment(reservationId: Long): PaymentCancelRequest {
|
||||||
log.debug { "[PaymentService.createCanceledPaymentByReservationId] 예약 삭제 & 결제 취소 정보 저장 시작: reservationId=$reservationId" }
|
log.debug { "[PaymentService.createCanceledPayment] 시작: reservationId=$reservationId" }
|
||||||
val paymentKey: String = paymentRepository.findPaymentKeyByReservationId(reservationId)
|
|
||||||
?: run {
|
|
||||||
log.warn { "[PaymentService.createCanceledPaymentByReservationId] 예약 조회 실패: reservationId=$reservationId" }
|
|
||||||
throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND)
|
|
||||||
}
|
|
||||||
|
|
||||||
val canceled: CanceledPaymentEntity = cancelPayment(paymentKey)
|
val payment: PaymentEntity = paymentFinder.findByReservationId(reservationId)
|
||||||
|
val canceled: CanceledPaymentEntity = paymentWriter.createCanceled(
|
||||||
return PaymentCancelRequest(paymentKey, canceled.cancelAmount, canceled.cancelReason)
|
payment = payment,
|
||||||
.also { log.info { "[PaymentService.createCanceledPaymentByReservationId] 예약 ID로 결제 취소 완료: reservationId=$reservationId" } }
|
cancelReason = "예약 취소",
|
||||||
}
|
canceledAt = OffsetDateTime.now(),
|
||||||
|
|
||||||
private fun cancelPayment(
|
|
||||||
paymentKey: String,
|
|
||||||
cancelReason: String = "고객 요청",
|
|
||||||
canceledAt: OffsetDateTime = OffsetDateTime.now(),
|
|
||||||
): CanceledPaymentEntity {
|
|
||||||
log.debug { "[PaymentService.cancelPayment] 결제 취소 정보 저장 시작: paymentKey=$paymentKey" }
|
|
||||||
val payment: PaymentEntity = paymentRepository.findByPaymentKey(paymentKey)
|
|
||||||
?.also {
|
|
||||||
paymentRepository.delete(it)
|
|
||||||
log.info { "[PaymentService.cancelPayment] 결제 정보 삭제 완료: paymentId=${it.id}, paymentKey=$paymentKey" }
|
|
||||||
}
|
|
||||||
?: run {
|
|
||||||
log.warn { "[PaymentService.cancelPayment] 결제 정보 조회 실패: paymentKey=$paymentKey" }
|
|
||||||
throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND)
|
|
||||||
}
|
|
||||||
|
|
||||||
val canceledPayment = CanceledPaymentEntity(
|
|
||||||
_id = tsidFactory.next(),
|
|
||||||
paymentKey = paymentKey,
|
|
||||||
cancelReason = cancelReason,
|
|
||||||
cancelAmount = payment.totalAmount,
|
|
||||||
approvedAt = payment.approvedAt,
|
|
||||||
canceledAt = canceledAt
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return canceledPaymentRepository.save(canceledPayment)
|
return PaymentCancelRequest(canceled.paymentKey, canceled.cancelAmount, canceled.cancelReason)
|
||||||
.also { log.info { "[PaymentService.cancelPayment] 결제 취소 정보 저장 완료: canceledPaymentId=${it.id}" } }
|
.also { log.info { "[PaymentService.createCanceledPayment] 완료: reservationId=$reservationId, paymentKey=${it.paymentKey}" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@ -132,18 +90,10 @@ class PaymentService(
|
|||||||
paymentKey: String,
|
paymentKey: String,
|
||||||
canceledAt: OffsetDateTime,
|
canceledAt: OffsetDateTime,
|
||||||
) {
|
) {
|
||||||
log.debug { "[PaymentService.updateCanceledTime] 취소 시간 업데이트 시작: paymentKey=$paymentKey, canceledAt=$canceledAt" }
|
log.debug { "[PaymentService.updateCanceledTime] 시작: paymentKey=$paymentKey, canceledAt=$canceledAt" }
|
||||||
canceledPaymentRepository.findByPaymentKey(paymentKey)
|
|
||||||
?.apply { this.canceledAt = canceledAt }
|
paymentFinder.findCanceledByKey(paymentKey).apply { this.canceledAt = canceledAt }
|
||||||
?.also {
|
|
||||||
log.info {
|
log.info { "[PaymentService.updateCanceledTime] 완료: paymentKey=$paymentKey, canceledAt=$canceledAt" }
|
||||||
"[PaymentService.updateCanceledTime] 취소 시간 업데이트 완료: paymentKey=$paymentKey" +
|
|
||||||
", canceledAt=$canceledAt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?: run {
|
|
||||||
log.warn { "[PaymentService.updateCanceledTime] 결제 정보 조회 실패: paymentKey=$paymentKey" }
|
|
||||||
throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,48 @@
|
|||||||
|
package roomescape.payment.implement
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import roomescape.payment.exception.PaymentErrorCode
|
||||||
|
import roomescape.payment.exception.PaymentException
|
||||||
|
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity
|
||||||
|
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository
|
||||||
|
import roomescape.payment.infrastructure.persistence.PaymentEntity
|
||||||
|
import roomescape.payment.infrastructure.persistence.PaymentRepository
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class PaymentFinder(
|
||||||
|
private val paymentRepository: PaymentRepository,
|
||||||
|
private val canceledPaymentRepository: CanceledPaymentRepository,
|
||||||
|
) {
|
||||||
|
fun existsPaymentByReservationId(reservationId: Long): Boolean {
|
||||||
|
log.debug { "[PaymentFinder.existsPaymentByReservationId] 시작: reservationId=$reservationId" }
|
||||||
|
|
||||||
|
return paymentRepository.existsByReservationId(reservationId)
|
||||||
|
.also { log.debug { "[PaymentFinder.existsPaymentByReservationId] 완료: reservationId=$reservationId, isExist=$it" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findByReservationId(reservationId: Long): PaymentEntity {
|
||||||
|
log.debug { "[PaymentFinder.findByReservationId] 시작: reservationId=$reservationId" }
|
||||||
|
|
||||||
|
return paymentRepository.findByReservationId(reservationId)
|
||||||
|
?.also { log.debug { "[PaymentFinder.findByReservationId] 완료: reservationId=$reservationId" } }
|
||||||
|
?: run {
|
||||||
|
log.warn { "[PaymentFinder.findByReservationId] 실패: reservationId=$reservationId" }
|
||||||
|
throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findCanceledByKey(paymentKey: String): CanceledPaymentEntity {
|
||||||
|
log.debug { "[PaymentFinder.findCanceledByKey] 시작: paymentKey=$paymentKey" }
|
||||||
|
|
||||||
|
return canceledPaymentRepository.findByPaymentKey(paymentKey)
|
||||||
|
?.also { log.debug { "[PaymentFinder.findCanceledByKey] 완료: canceledPaymentId=${it.id}" } }
|
||||||
|
?: run {
|
||||||
|
log.warn { "[PaymentFinder.findCanceledByKey] 실패: paymentKey=$paymentKey" }
|
||||||
|
throw PaymentException(PaymentErrorCode.CANCELED_PAYMENT_NOT_FOUND)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
package roomescape.payment.implement
|
||||||
|
|
||||||
|
import com.github.f4b6a3.tsid.TsidFactory
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import roomescape.common.config.next
|
||||||
|
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity
|
||||||
|
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository
|
||||||
|
import roomescape.payment.infrastructure.persistence.PaymentEntity
|
||||||
|
import roomescape.payment.infrastructure.persistence.PaymentRepository
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class PaymentWriter(
|
||||||
|
private val paymentRepository: PaymentRepository,
|
||||||
|
private val canceledPaymentRepository: CanceledPaymentRepository,
|
||||||
|
private val tsidFactory: TsidFactory,
|
||||||
|
) {
|
||||||
|
fun create(
|
||||||
|
paymentKey: String,
|
||||||
|
orderId: String,
|
||||||
|
totalAmount: Long,
|
||||||
|
approvedAt: OffsetDateTime,
|
||||||
|
reservation: ReservationEntity
|
||||||
|
): PaymentEntity {
|
||||||
|
log.debug { "[PaymentWriter.create] 시작: paymentKey=${paymentKey}, reservationId=${reservation.id}" }
|
||||||
|
|
||||||
|
val payment = PaymentEntity(
|
||||||
|
_id = tsidFactory.next(),
|
||||||
|
orderId = orderId,
|
||||||
|
paymentKey = paymentKey,
|
||||||
|
totalAmount = totalAmount,
|
||||||
|
reservation = reservation,
|
||||||
|
approvedAt = approvedAt
|
||||||
|
)
|
||||||
|
|
||||||
|
return paymentRepository.save(payment)
|
||||||
|
.also { log.debug { "[PaymentWriter.create] 완료: paymentId=${it.id}, reservationId=${reservation.id}" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createCanceled(
|
||||||
|
payment: PaymentEntity,
|
||||||
|
cancelReason: String,
|
||||||
|
canceledAt: OffsetDateTime,
|
||||||
|
): CanceledPaymentEntity = createCanceled(
|
||||||
|
cancelReason = cancelReason,
|
||||||
|
canceledAt = canceledAt,
|
||||||
|
cancelAmount = payment.totalAmount,
|
||||||
|
approvedAt = payment.approvedAt,
|
||||||
|
paymentKey = payment.paymentKey
|
||||||
|
)
|
||||||
|
|
||||||
|
fun createCanceled(
|
||||||
|
cancelReason: String,
|
||||||
|
cancelAmount: Long,
|
||||||
|
canceledAt: OffsetDateTime,
|
||||||
|
approvedAt: OffsetDateTime,
|
||||||
|
paymentKey: String,
|
||||||
|
): CanceledPaymentEntity {
|
||||||
|
log.debug { "[PaymentWriter.createCanceled] 시작: paymentKey=$paymentKey cancelAmount=$cancelAmount" }
|
||||||
|
|
||||||
|
val canceledPayment = CanceledPaymentEntity(
|
||||||
|
_id = tsidFactory.next(),
|
||||||
|
paymentKey = paymentKey,
|
||||||
|
cancelReason = cancelReason,
|
||||||
|
cancelAmount = cancelAmount,
|
||||||
|
approvedAt = approvedAt,
|
||||||
|
canceledAt = canceledAt
|
||||||
|
)
|
||||||
|
|
||||||
|
return canceledPaymentRepository.save(canceledPayment)
|
||||||
|
.also {
|
||||||
|
paymentRepository.deleteByPaymentKey(paymentKey)
|
||||||
|
log.debug { "[PaymentWriter.createCanceled] 완료: paymentKey=${paymentKey}, canceledPaymentId=${it.id}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,14 +1,12 @@
|
|||||||
package roomescape.payment.infrastructure.persistence
|
package roomescape.payment.infrastructure.persistence
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
import org.springframework.data.jpa.repository.Query
|
|
||||||
|
|
||||||
interface PaymentRepository : JpaRepository<PaymentEntity, Long> {
|
interface PaymentRepository : JpaRepository<PaymentEntity, Long> {
|
||||||
|
|
||||||
fun existsByReservationId(reservationId: Long): Boolean
|
fun existsByReservationId(reservationId: Long): Boolean
|
||||||
|
|
||||||
@Query("SELECT p.paymentKey FROM PaymentEntity p WHERE p.reservation.id = :reservationId")
|
fun findByReservationId(reservationId: Long): PaymentEntity?
|
||||||
fun findPaymentKeyByReservationId(reservationId: Long): String?
|
|
||||||
|
|
||||||
fun findByPaymentKey(paymentKey: String): PaymentEntity?
|
fun deleteByPaymentKey(paymentKey: String)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,6 @@ package roomescape.payment.web
|
|||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||||
import roomescape.payment.infrastructure.client.PaymentCancelResponseDeserializer
|
import roomescape.payment.infrastructure.client.PaymentCancelResponseDeserializer
|
||||||
import roomescape.payment.infrastructure.persistence.PaymentEntity
|
import roomescape.payment.infrastructure.persistence.PaymentEntity
|
||||||
import roomescape.reservation.web.ReservationRetrieveResponse
|
|
||||||
import roomescape.reservation.web.toRetrieveResponse
|
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
data class PaymentCancelRequest(
|
data class PaymentCancelRequest(
|
||||||
@ -26,15 +24,15 @@ data class PaymentCreateResponse(
|
|||||||
val orderId: String,
|
val orderId: String,
|
||||||
val paymentKey: String,
|
val paymentKey: String,
|
||||||
val totalAmount: Long,
|
val totalAmount: Long,
|
||||||
val reservation: ReservationRetrieveResponse,
|
val reservationId: Long,
|
||||||
val approvedAt: OffsetDateTime
|
val approvedAt: OffsetDateTime
|
||||||
)
|
)
|
||||||
|
|
||||||
fun PaymentEntity.toCreateResponse(): PaymentCreateResponse = PaymentCreateResponse(
|
fun PaymentEntity.toCreateResponse() = PaymentCreateResponse(
|
||||||
id = this.id!!,
|
id = this.id!!,
|
||||||
orderId = this.orderId,
|
orderId = this.orderId,
|
||||||
paymentKey = this.paymentKey,
|
paymentKey = this.paymentKey,
|
||||||
totalAmount = this.totalAmount,
|
totalAmount = this.totalAmount,
|
||||||
reservation = this.reservation.toRetrieveResponse(),
|
reservationId = this.reservation.id!!,
|
||||||
approvedAt = this.approvedAt
|
approvedAt = this.approvedAt
|
||||||
)
|
)
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
package roomescape.reservation.business
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import roomescape.reservation.implement.ReservationFinder
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||||
|
import roomescape.reservation.web.MyReservationRetrieveListResponse
|
||||||
|
import roomescape.reservation.web.ReservationRetrieveListResponse
|
||||||
|
import roomescape.reservation.web.toRetrieveListResponse
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
private val log = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
class ReservationFindService(
|
||||||
|
private val reservationFinder: ReservationFinder
|
||||||
|
) {
|
||||||
|
fun findReservations(): ReservationRetrieveListResponse {
|
||||||
|
log.debug { "[ReservationService.findReservations] 시작" }
|
||||||
|
|
||||||
|
return reservationFinder.findAllByStatuses(*ReservationStatus.confirmedStatus())
|
||||||
|
.toRetrieveListResponse()
|
||||||
|
.also { log.info { "[ReservationService.findReservations] ${it.reservations.size}개의 예약 조회 완료" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findAllWaiting(): ReservationRetrieveListResponse {
|
||||||
|
log.debug { "[ReservationService.findAllWaiting] 시작" }
|
||||||
|
|
||||||
|
return reservationFinder.findAllByStatuses(ReservationStatus.WAITING)
|
||||||
|
.toRetrieveListResponse()
|
||||||
|
.also { log.info { "[ReservationService.findAllWaiting] ${it.reservations.size}개의 대기 조회 완료" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findReservationsByMemberId(memberId: Long): MyReservationRetrieveListResponse {
|
||||||
|
log.debug { "[ReservationService.findReservationsByMemberId] 시작: memberId=$memberId" }
|
||||||
|
|
||||||
|
return reservationFinder.findAllByMemberId(memberId)
|
||||||
|
.toRetrieveListResponse()
|
||||||
|
.also { log.info { "[ReservationService.findReservationsByMemberId] ${it.reservations.size}개의 예약 조회 완료: memberId=$memberId" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun searchReservations(
|
||||||
|
themeId: Long?,
|
||||||
|
memberId: Long?,
|
||||||
|
startFrom: LocalDate?,
|
||||||
|
endAt: LocalDate?,
|
||||||
|
): ReservationRetrieveListResponse {
|
||||||
|
log.debug { "[ReservationService.searchReservations] 시작: themeId=$themeId, memberId=$memberId, dateFrom=$startFrom, dateTo=$endAt" }
|
||||||
|
|
||||||
|
return reservationFinder.searchReservations(themeId, memberId, startFrom, endAt)
|
||||||
|
.toRetrieveListResponse()
|
||||||
|
.also { log.info { "[ReservationService.searchReservations] ${it.reservations.size}개의 예약 조회 완료: themeId=$themeId, memberId=$memberId, dateFrom=$startFrom, dateTo=$endAt" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,322 +0,0 @@
|
|||||||
package roomescape.reservation.business
|
|
||||||
|
|
||||||
import com.github.f4b6a3.tsid.TsidFactory
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
|
||||||
import org.springframework.data.jpa.domain.Specification
|
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
|
||||||
import org.springframework.stereotype.Service
|
|
||||||
import org.springframework.transaction.annotation.Transactional
|
|
||||||
import roomescape.common.config.next
|
|
||||||
import roomescape.member.business.MemberService
|
|
||||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
|
||||||
import roomescape.reservation.exception.ReservationErrorCode
|
|
||||||
import roomescape.reservation.exception.ReservationException
|
|
||||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
|
||||||
import roomescape.reservation.infrastructure.persistence.ReservationRepository
|
|
||||||
import roomescape.reservation.infrastructure.persistence.ReservationSearchSpecification
|
|
||||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
|
||||||
import roomescape.reservation.web.*
|
|
||||||
import roomescape.theme.business.ThemeService
|
|
||||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
|
||||||
import roomescape.time.business.TimeService
|
|
||||||
import roomescape.time.infrastructure.persistence.TimeEntity
|
|
||||||
import java.time.LocalDate
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
|
|
||||||
private val log = KotlinLogging.logger {}
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@Transactional
|
|
||||||
class ReservationService(
|
|
||||||
private val tsidFactory: TsidFactory,
|
|
||||||
private val reservationRepository: ReservationRepository,
|
|
||||||
private val timeService: TimeService,
|
|
||||||
private val memberService: MemberService,
|
|
||||||
private val themeService: ThemeService,
|
|
||||||
) {
|
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
fun findReservations(): ReservationRetrieveListResponse {
|
|
||||||
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
|
||||||
.confirmed()
|
|
||||||
.build()
|
|
||||||
val reservations = findAllReservationByStatus(spec)
|
|
||||||
log.info { "[ReservationService.findReservations] ${reservations.size} 개의 확정 예약 조회 완료" }
|
|
||||||
|
|
||||||
return ReservationRetrieveListResponse(reservations)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
fun findAllWaiting(): ReservationRetrieveListResponse {
|
|
||||||
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
|
||||||
.waiting()
|
|
||||||
.build()
|
|
||||||
val reservations = findAllReservationByStatus(spec)
|
|
||||||
log.info { "[ReservationService.findAllWaiting] ${reservations.size} 개의 대기 예약 조회 완료" }
|
|
||||||
|
|
||||||
return ReservationRetrieveListResponse(reservations)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findAllReservationByStatus(spec: Specification<ReservationEntity>): List<ReservationRetrieveResponse> {
|
|
||||||
return reservationRepository.findAll(spec).map { it.toRetrieveResponse() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteReservation(reservationId: Long, memberId: Long) {
|
|
||||||
validateIsMemberAdmin(memberId, "deleteReservation")
|
|
||||||
log.debug { "[ReservationService.deleteReservation] 예약 삭제 시작: reservationId=$reservationId, memberId=$memberId" }
|
|
||||||
reservationRepository.deleteById(reservationId)
|
|
||||||
log.info { "[ReservationService.deleteReservation] 예약 삭제 완료: reservationId=$reservationId" }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createConfirmedReservation(
|
|
||||||
request: ReservationCreateWithPaymentRequest,
|
|
||||||
memberId: Long,
|
|
||||||
): ReservationEntity {
|
|
||||||
val themeId = request.themeId
|
|
||||||
val timeId = request.timeId
|
|
||||||
val date: LocalDate = request.date
|
|
||||||
validateIsReservationExist(themeId, timeId, date, "createConfirmedReservation")
|
|
||||||
|
|
||||||
log.debug { "[ReservationService.createConfirmedReservation] 예약 추가 시작: memberId=$memberId, themeId=${request.themeId}, timeId=${request.timeId}, date=${request.date}" }
|
|
||||||
val reservation: ReservationEntity =
|
|
||||||
createEntity(timeId, themeId, date, memberId, ReservationStatus.CONFIRMED)
|
|
||||||
|
|
||||||
return reservationRepository.save(reservation)
|
|
||||||
.also { log.info { "[ReservationService.createConfirmedReservation] 예약 추가 완료: reservationId=${it.id}, status=${it.status}" } }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createReservationByAdmin(request: AdminReservationCreateRequest): ReservationRetrieveResponse {
|
|
||||||
validateIsReservationExist(request.themeId, request.timeId, request.date)
|
|
||||||
|
|
||||||
log.debug { "[ReservationService.createReservationByAdmin] 관리자의 예약 추가: memberId=${request.memberId}, themeId=${request.themeId}, timeId=${request.timeId}, date=${request.date}" }
|
|
||||||
return addReservationWithoutPayment(
|
|
||||||
request.themeId,
|
|
||||||
request.timeId,
|
|
||||||
request.date,
|
|
||||||
request.memberId,
|
|
||||||
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
|
||||||
).also {
|
|
||||||
log.info { "[ReservationService.createReservationByAdmin] 관리자 예약 추가 완료: reservationId=${it.id}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createWaiting(request: WaitingCreateRequest, memberId: Long): ReservationRetrieveResponse {
|
|
||||||
validateMemberAlreadyReserve(request.themeId, request.timeId, request.date, memberId)
|
|
||||||
log.debug { "[ReservationService.createWaiting] 예약 대기 추가 시작: memberId=$memberId, themeId=${request.themeId}, timeId=${request.timeId}, date=${request.date}" }
|
|
||||||
return addReservationWithoutPayment(
|
|
||||||
request.themeId,
|
|
||||||
request.timeId,
|
|
||||||
request.date,
|
|
||||||
memberId,
|
|
||||||
ReservationStatus.WAITING
|
|
||||||
).also {
|
|
||||||
log.info { "[ReservationService.createWaiting] 예약 대기 추가 완료: reservationId=${it.id}, status=${it.status}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addReservationWithoutPayment(
|
|
||||||
themeId: Long,
|
|
||||||
timeId: Long,
|
|
||||||
date: LocalDate,
|
|
||||||
memberId: Long,
|
|
||||||
status: ReservationStatus,
|
|
||||||
): ReservationRetrieveResponse = createEntity(timeId, themeId, date, memberId, status)
|
|
||||||
.also {
|
|
||||||
reservationRepository.save(it)
|
|
||||||
}.toRetrieveResponse()
|
|
||||||
|
|
||||||
private fun validateMemberAlreadyReserve(themeId: Long, timeId: Long, date: LocalDate, memberId: Long) {
|
|
||||||
log.debug {
|
|
||||||
"[ReservationService.validateMemberAlreadyReserve] 회원의 중복 예약 여부 확인: themeId=$themeId, timeId=$timeId, date=$date, memberId=$memberId"
|
|
||||||
}
|
|
||||||
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
|
||||||
.sameMemberId(memberId)
|
|
||||||
.sameThemeId(themeId)
|
|
||||||
.sameTimeId(timeId)
|
|
||||||
.sameDate(date)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
if (reservationRepository.exists(spec)) {
|
|
||||||
log.warn { "[ReservationService.validateMemberAlreadyReserve] 중복된 예약 존재: themeId=$themeId, timeId=$timeId, date=$date" }
|
|
||||||
throw ReservationException(ReservationErrorCode.ALREADY_RESERVE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateIsReservationExist(
|
|
||||||
themeId: Long,
|
|
||||||
timeId: Long,
|
|
||||||
date: LocalDate,
|
|
||||||
calledBy: String = "validateIsReservationExist"
|
|
||||||
) {
|
|
||||||
log.debug {
|
|
||||||
"[ReservationService.$calledBy] 예약 존재 여부 확인: themeId=$themeId, timeId=$timeId, date=$date"
|
|
||||||
}
|
|
||||||
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
|
||||||
.confirmed()
|
|
||||||
.sameThemeId(themeId)
|
|
||||||
.sameTimeId(timeId)
|
|
||||||
.sameDate(date)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
if (reservationRepository.exists(spec)) {
|
|
||||||
log.warn { "[ReservationService.$calledBy] 중복된 예약 존재: themeId=$themeId, timeId=$timeId, date=$date" }
|
|
||||||
throw ReservationException(ReservationErrorCode.RESERVATION_DUPLICATED)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateDateAndTime(
|
|
||||||
requestDate: LocalDate,
|
|
||||||
requestTime: TimeEntity,
|
|
||||||
) {
|
|
||||||
val now = LocalDateTime.now()
|
|
||||||
val request = LocalDateTime.of(requestDate, requestTime.startAt)
|
|
||||||
|
|
||||||
if (request.isBefore(now)) {
|
|
||||||
log.info { "[ReservationService.validateDateAndTime] 날짜 범위 오류. request=$request, now=$now" }
|
|
||||||
throw ReservationException(ReservationErrorCode.PAST_REQUEST_DATETIME)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createEntity(
|
|
||||||
timeId: Long,
|
|
||||||
themeId: Long,
|
|
||||||
date: LocalDate,
|
|
||||||
memberId: Long,
|
|
||||||
status: ReservationStatus,
|
|
||||||
): ReservationEntity {
|
|
||||||
val time: TimeEntity = timeService.findById(timeId)
|
|
||||||
val theme: ThemeEntity = themeService.findById(themeId)
|
|
||||||
val member: MemberEntity = memberService.findById(memberId)
|
|
||||||
|
|
||||||
validateDateAndTime(date, time)
|
|
||||||
|
|
||||||
return ReservationEntity(
|
|
||||||
_id = tsidFactory.next(),
|
|
||||||
date = date,
|
|
||||||
time = time,
|
|
||||||
theme = theme,
|
|
||||||
member = member,
|
|
||||||
status = status
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
fun searchReservations(
|
|
||||||
themeId: Long?,
|
|
||||||
memberId: Long?,
|
|
||||||
dateFrom: LocalDate?,
|
|
||||||
dateTo: LocalDate?,
|
|
||||||
): ReservationRetrieveListResponse {
|
|
||||||
log.debug { "[ReservationService.searchReservations] 예약 검색 시작: themeId=$themeId, memberId=$memberId, dateFrom=$dateFrom, dateTo=$dateTo" }
|
|
||||||
validateSearchDateRange(dateFrom, dateTo)
|
|
||||||
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
|
||||||
.confirmed()
|
|
||||||
.sameThemeId(themeId)
|
|
||||||
.sameMemberId(memberId)
|
|
||||||
.dateStartFrom(dateFrom)
|
|
||||||
.dateEndAt(dateTo)
|
|
||||||
.build()
|
|
||||||
val reservations = findAllReservationByStatus(spec)
|
|
||||||
|
|
||||||
return ReservationRetrieveListResponse(reservations)
|
|
||||||
.also { log.info { "[ReservationService.searchReservations] 예약 ${reservations.size}개 조회 완료: themeId=$themeId, memberId=$memberId, dateFrom=$dateFrom, dateTo=$dateTo" } }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateSearchDateRange(startFrom: LocalDate?, endAt: LocalDate?) {
|
|
||||||
if (startFrom == null || endAt == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (startFrom.isAfter(endAt)) {
|
|
||||||
log.info { "[ReservationService.validateSearchDateRange] 조회 범위 오류: startFrom=$startFrom, endAt=$endAt" }
|
|
||||||
throw ReservationException(ReservationErrorCode.INVALID_SEARCH_DATE_RANGE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
fun findReservationsByMemberId(memberId: Long): MyReservationRetrieveListResponse {
|
|
||||||
val reservations = reservationRepository.findAllByMemberId(memberId)
|
|
||||||
log.info { "[ReservationService.findReservationsByMemberId] memberId=${memberId}인 ${reservations.size}개의 예약 조회 완료" }
|
|
||||||
return MyReservationRetrieveListResponse(reservations)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun confirmWaiting(reservationId: Long, memberId: Long) {
|
|
||||||
log.debug { "[ReservationService.confirmWaiting] 대기 예약 승인 시작: reservationId=$reservationId (by adminId=$memberId)" }
|
|
||||||
validateIsMemberAdmin(memberId, "confirmWaiting")
|
|
||||||
|
|
||||||
log.debug { "[ReservationService.confirmWaiting] 대기 여부 확인 시작: reservationId=$reservationId" }
|
|
||||||
if (reservationRepository.isExistConfirmedReservation(reservationId)) {
|
|
||||||
log.warn { "[ReservationService.confirmWaiting] 승인 실패(이미 확정된 예약 존재): reservationId=$reservationId" }
|
|
||||||
throw ReservationException(ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug { "[ReservationService.confirmWaiting] 대기 예약 상태 변경 시작: reservationId=$reservationId" }
|
|
||||||
reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)
|
|
||||||
log.debug { "[ReservationService.confirmWaiting] 대기 예약 상태 변경 완료: reservationId=$reservationId, status=${ReservationStatus.CONFIRMED_PAYMENT_REQUIRED}" }
|
|
||||||
|
|
||||||
log.info { "[ReservationService.confirmWaiting] 대기 예약 승인 완료: reservationId=$reservationId" }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteWaiting(reservationId: Long, memberId: Long) {
|
|
||||||
log.debug { "[ReservationService.deleteWaiting] 대기 취소 시작: reservationId=$reservationId, memberId=$memberId" }
|
|
||||||
|
|
||||||
val reservation: ReservationEntity = findReservationOrThrow(reservationId, "deleteWaiting")
|
|
||||||
if (!reservation.isWaiting()) {
|
|
||||||
log.warn {
|
|
||||||
"[ReservationService.deleteWaiting] 대기 취소 실패(대기 예약이 아님): reservationId=$reservationId" +
|
|
||||||
", currentStatus=${reservation.status} memberId=$memberId"
|
|
||||||
}
|
|
||||||
throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED)
|
|
||||||
}
|
|
||||||
if (!reservation.isReservedBy(memberId)) {
|
|
||||||
log.error {
|
|
||||||
"[ReservationService.deleteWaiting] 대기 취소 실패(예약자 본인의 취소 요청이 아님): reservationId=$reservationId" +
|
|
||||||
", memberId=$memberId "
|
|
||||||
}
|
|
||||||
throw ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER)
|
|
||||||
}
|
|
||||||
log.debug { "[ReservationService.deleteWaiting] 대기 예약 삭제 시작: reservationId=$reservationId" }
|
|
||||||
reservationRepository.delete(reservation)
|
|
||||||
|
|
||||||
log.info { "[ReservationService.deleteWaiting] 대기 취소 완료: reservationId=$reservationId, memberId=$memberId" }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun rejectWaiting(reservationId: Long, memberId: Long) {
|
|
||||||
validateIsMemberAdmin(memberId, "rejectWaiting")
|
|
||||||
log.debug { "[ReservationService.rejectWaiting] 대기 예약 삭제 시작: reservationId=$reservationId (by adminId=$memberId)" }
|
|
||||||
val reservation: ReservationEntity = findReservationOrThrow(reservationId, "rejectWaiting")
|
|
||||||
|
|
||||||
if (!reservation.isWaiting()) {
|
|
||||||
log.warn {
|
|
||||||
"[ReservationService.rejectWaiting] 대기 예약 삭제 실패(이미 확정 상태): reservationId=$reservationId" +
|
|
||||||
", status=${reservation.status}"
|
|
||||||
}
|
|
||||||
throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED)
|
|
||||||
}
|
|
||||||
reservationRepository.delete(reservation)
|
|
||||||
log.info { "[ReservationService.rejectWaiting] 대기 예약 삭제 완료: reservationId=$reservationId" }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateIsMemberAdmin(memberId: Long, calledBy: String = "validateIsMemberAdmin") {
|
|
||||||
log.debug { "[ReservationService.$calledBy] 관리자 여부 확인: memberId=$memberId" }
|
|
||||||
val member: MemberEntity = memberService.findById(memberId)
|
|
||||||
if (member.isAdmin()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.warn { "[ReservationService.$calledBy] 관리자가 아님: memberId=$memberId, role=${member.role}" }
|
|
||||||
throw ReservationException(ReservationErrorCode.NO_PERMISSION)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findReservationOrThrow(
|
|
||||||
reservationId: Long,
|
|
||||||
calledBy: String = "findReservationOrThrow"
|
|
||||||
): ReservationEntity {
|
|
||||||
log.debug { "[ReservationService.$calledBy] 예약 조회: reservationId=$reservationId" }
|
|
||||||
return reservationRepository.findByIdOrNull(reservationId)
|
|
||||||
?.also { log.info { "[ReservationService.$calledBy] 예약 조회 완료: reservationId=$reservationId" } }
|
|
||||||
?: run {
|
|
||||||
log.warn { "[ReservationService.$calledBy] 예약 조회 실패: reservationId=$reservationId" }
|
|
||||||
throw ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -8,8 +8,9 @@ import roomescape.payment.infrastructure.client.PaymentApproveResponse
|
|||||||
import roomescape.payment.web.PaymentCancelRequest
|
import roomescape.payment.web.PaymentCancelRequest
|
||||||
import roomescape.payment.web.PaymentCancelResponse
|
import roomescape.payment.web.PaymentCancelResponse
|
||||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||||
|
import roomescape.reservation.web.ReservationCreateResponse
|
||||||
import roomescape.reservation.web.ReservationCreateWithPaymentRequest
|
import roomescape.reservation.web.ReservationCreateWithPaymentRequest
|
||||||
import roomescape.reservation.web.ReservationRetrieveResponse
|
import roomescape.reservation.web.toCreateResponse
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
private val log = KotlinLogging.logger {}
|
private val log = KotlinLogging.logger {}
|
||||||
@ -17,47 +18,52 @@ private val log = KotlinLogging.logger {}
|
|||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Transactional
|
||||||
class ReservationWithPaymentService(
|
class ReservationWithPaymentService(
|
||||||
private val reservationService: ReservationService,
|
private val reservationWriteService: ReservationWriteService,
|
||||||
private val paymentService: PaymentService,
|
private val paymentService: PaymentService,
|
||||||
) {
|
) {
|
||||||
fun createReservationAndPayment(
|
fun createReservationAndPayment(
|
||||||
request: ReservationCreateWithPaymentRequest,
|
request: ReservationCreateWithPaymentRequest,
|
||||||
paymentInfo: PaymentApproveResponse,
|
approvedPaymentInfo: PaymentApproveResponse,
|
||||||
memberId: Long,
|
memberId: Long,
|
||||||
): ReservationRetrieveResponse {
|
): ReservationCreateResponse {
|
||||||
log.info { "[ReservationWithPaymentService.createReservationAndPayment] 예약 & 결제 정보 저장 시작: memberId=$memberId, paymentInfo=$paymentInfo" }
|
log.info { "[ReservationWithPaymentService.createReservationAndPayment] 시작: memberId=$memberId, paymentInfo=$approvedPaymentInfo" }
|
||||||
val reservation: ReservationEntity = reservationService.createConfirmedReservation(request, memberId)
|
|
||||||
|
|
||||||
return paymentService.createPayment(paymentInfo, reservation)
|
val reservation: ReservationEntity = reservationWriteService.createReservationWithPayment(request, memberId)
|
||||||
.also { log.info { "[ReservationWithPaymentService.createReservationAndPayment] 예약 & 결제 정보 저장 완료: reservationId=${reservation.id}, paymentId=${it.id}" } }
|
.also { paymentService.createPayment(approvedPaymentInfo, it) }
|
||||||
.reservation
|
|
||||||
|
return reservation.toCreateResponse()
|
||||||
|
.also { log.info { "[ReservationWithPaymentService.createReservationAndPayment] 완료: reservationId=${reservation.id}, paymentId=${it.id}" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createCanceledPayment(
|
fun createCanceledPayment(
|
||||||
cancelInfo: PaymentCancelResponse,
|
canceledPaymentInfo: PaymentCancelResponse,
|
||||||
approvedAt: OffsetDateTime,
|
approvedAt: OffsetDateTime,
|
||||||
paymentKey: String,
|
paymentKey: String,
|
||||||
) {
|
) {
|
||||||
paymentService.createCanceledPayment(cancelInfo, approvedAt, paymentKey)
|
paymentService.createCanceledPayment(canceledPaymentInfo, approvedAt, paymentKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteReservationAndPayment(
|
fun deleteReservationAndPayment(
|
||||||
reservationId: Long,
|
reservationId: Long,
|
||||||
memberId: Long,
|
memberId: Long,
|
||||||
): PaymentCancelRequest {
|
): PaymentCancelRequest {
|
||||||
log.info { "[ReservationWithPaymentService.deleteReservationAndPayment] 결제 취소 정보 저장 & 예약 삭제 시작: reservationId=$reservationId" }
|
log.info { "[ReservationWithPaymentService.deleteReservationAndPayment] 시작: reservationId=$reservationId" }
|
||||||
val paymentCancelRequest = paymentService.createCanceledPaymentByReservationId(reservationId)
|
val paymentCancelRequest = paymentService.createCanceledPayment(reservationId)
|
||||||
|
|
||||||
reservationService.deleteReservation(reservationId, memberId)
|
reservationWriteService.deleteReservation(reservationId, memberId)
|
||||||
log.info { "[ReservationWithPaymentService.deleteReservationAndPayment] 결제 취소 정보 저장 & 예약 삭제 완료: reservationId=$reservationId" }
|
log.info { "[ReservationWithPaymentService.deleteReservationAndPayment] 완료: reservationId=$reservationId" }
|
||||||
return paymentCancelRequest
|
return paymentCancelRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun isNotPaidReservation(reservationId: Long): Boolean {
|
fun isNotPaidReservation(reservationId: Long): Boolean {
|
||||||
log.debug { "[ReservationWithPaymentService.isNotPaidReservation] 예약 결제 여부 확인: reservationId=$reservationId" }
|
log.info { "[ReservationWithPaymentService.isNotPaidReservation] 시작: reservationId=$reservationId" }
|
||||||
return !paymentService.isReservationPaid(reservationId)
|
|
||||||
.also { log.info { "[ReservationWithPaymentService.isNotPaidReservation] 결제 여부 확인 완료: reservationId=$reservationId, 결제 여부=${!it}" } }
|
val notPaid: Boolean = !paymentService.existsByReservationId(reservationId)
|
||||||
|
|
||||||
|
return notPaid.also {
|
||||||
|
log.info { "[ReservationWithPaymentService.isNotPaidReservation] 완료: reservationId=$reservationId, isPaid=${notPaid}" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateCanceledTime(
|
fun updateCanceledTime(
|
||||||
|
|||||||
@ -0,0 +1,104 @@
|
|||||||
|
package roomescape.reservation.business
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import jakarta.transaction.Transactional
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import roomescape.reservation.implement.ReservationFinder
|
||||||
|
import roomescape.reservation.implement.ReservationWriter
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||||
|
import roomescape.reservation.web.*
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Transactional
|
||||||
|
class ReservationWriteService(
|
||||||
|
private val reservationFinder: ReservationFinder,
|
||||||
|
private val reservationWriter: ReservationWriter
|
||||||
|
) {
|
||||||
|
fun createReservationWithPayment(
|
||||||
|
request: ReservationCreateWithPaymentRequest,
|
||||||
|
memberId: Long
|
||||||
|
): ReservationEntity {
|
||||||
|
log.debug { "[ReservationCommandService.createReservationByAdmin] 시작: date=${request.date}, timeId=${request.timeId}, themeId=${request.themeId}, memberId=${memberId}" }
|
||||||
|
|
||||||
|
val created: ReservationEntity = reservationWriter.create(
|
||||||
|
date = request.date,
|
||||||
|
timeId = request.timeId,
|
||||||
|
themeId = request.themeId,
|
||||||
|
status = ReservationStatus.CONFIRMED,
|
||||||
|
memberId = memberId,
|
||||||
|
requesterId = memberId
|
||||||
|
)
|
||||||
|
|
||||||
|
return created.also {
|
||||||
|
log.info { "[ReservationCommandService.createReservationByAdmin] 완료: reservationId=${it.id}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createReservationByAdmin(
|
||||||
|
request: AdminReservationCreateRequest,
|
||||||
|
memberId: Long
|
||||||
|
): ReservationCreateResponse {
|
||||||
|
log.debug { "[ReservationCommandService.createReservationByAdmin] 시작: date=${request.date}, timeId=${request.timeId}, themeId=${request.themeId}, memberId=${request.memberId} by adminId=${memberId}" }
|
||||||
|
|
||||||
|
val created: ReservationEntity = reservationWriter.create(
|
||||||
|
date = request.date,
|
||||||
|
timeId = request.timeId,
|
||||||
|
themeId = request.themeId,
|
||||||
|
status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED,
|
||||||
|
memberId = request.memberId,
|
||||||
|
requesterId = memberId
|
||||||
|
)
|
||||||
|
|
||||||
|
return created.toCreateResponse()
|
||||||
|
.also {
|
||||||
|
log.info { "[ReservationCommandService.createReservationByAdmin] 관리자 예약 추가 완료: reservationId=${it.id}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createWaiting(request: WaitingCreateRequest, memberId: Long): ReservationCreateResponse {
|
||||||
|
log.debug { "[ReservationCommandService.createWaiting] 시작: date=${request.date}, timeId=${request.timeId}, themeId=${request.themeId}, memberId=${memberId}" }
|
||||||
|
|
||||||
|
val created: ReservationEntity = reservationWriter.create(
|
||||||
|
date = request.date,
|
||||||
|
timeId = request.timeId,
|
||||||
|
themeId = request.themeId,
|
||||||
|
status = ReservationStatus.WAITING,
|
||||||
|
memberId = memberId,
|
||||||
|
requesterId = memberId
|
||||||
|
)
|
||||||
|
|
||||||
|
return created.toCreateResponse()
|
||||||
|
.also {
|
||||||
|
log.info { "[ReservationCommandService.createWaiting] 완료: reservationId=${it.id}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteReservation(reservationId: Long, memberId: Long) {
|
||||||
|
log.debug { "[ReservationCommandService.deleteReservation] 시작: reservationId=${reservationId}, memberId=$memberId" }
|
||||||
|
|
||||||
|
val reservation: ReservationEntity = reservationFinder.findById(reservationId)
|
||||||
|
|
||||||
|
reservationWriter.deleteConfirmed(reservation, requesterId = memberId)
|
||||||
|
.also { log.info { "[ReservationCommandService.deleteReservation] 완료: reservationId=${reservationId}" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun confirmWaiting(reservationId: Long, memberId: Long) {
|
||||||
|
log.debug { "[ReservationCommandService.confirmWaiting] 시작: reservationId=$reservationId (by adminId=$memberId)" }
|
||||||
|
|
||||||
|
reservationWriter.confirm(reservationId)
|
||||||
|
.also { log.info { "[ReservationCommandService.confirmWaiting] 완료: reservationId=$reservationId" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteWaiting(reservationId: Long, memberId: Long) {
|
||||||
|
log.debug { "[ReservationCommandService.deleteWaiting] 시작: reservationId=$reservationId (by adminId=$memberId)" }
|
||||||
|
|
||||||
|
val reservation: ReservationEntity = reservationFinder.findById(reservationId)
|
||||||
|
|
||||||
|
reservationWriter.deleteWaiting(reservation, requesterId = memberId)
|
||||||
|
.also { log.info { "[ReservationCommandService.deleteWaiting] 완료: reservationId=$reservationId" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -74,7 +74,7 @@ interface ReservationAPI {
|
|||||||
fun createReservationWithPayment(
|
fun createReservationWithPayment(
|
||||||
@Valid @RequestBody reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest,
|
@Valid @RequestBody reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest,
|
||||||
@MemberId @Parameter(hidden = true) memberId: Long
|
@MemberId @Parameter(hidden = true) memberId: Long
|
||||||
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>>
|
): ResponseEntity<CommonApiResponse<ReservationCreateResponse>>
|
||||||
|
|
||||||
@Admin
|
@Admin
|
||||||
@Operation(summary = "관리자 예약 추가", tags = ["관리자 로그인이 필요한 API"])
|
@Operation(summary = "관리자 예약 추가", tags = ["관리자 로그인이 필요한 API"])
|
||||||
@ -92,7 +92,8 @@ interface ReservationAPI {
|
|||||||
)
|
)
|
||||||
fun createReservationByAdmin(
|
fun createReservationByAdmin(
|
||||||
@Valid @RequestBody adminReservationRequest: AdminReservationCreateRequest,
|
@Valid @RequestBody adminReservationRequest: AdminReservationCreateRequest,
|
||||||
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>>
|
@MemberId @Parameter(hidden = true) memberId: Long
|
||||||
|
): ResponseEntity<CommonApiResponse<ReservationCreateResponse>>
|
||||||
|
|
||||||
@Admin
|
@Admin
|
||||||
@Operation(summary = "모든 예약 대기 조회", tags = ["관리자 로그인이 필요한 API"])
|
@Operation(summary = "모든 예약 대기 조회", tags = ["관리자 로그인이 필요한 API"])
|
||||||
@ -116,7 +117,7 @@ interface ReservationAPI {
|
|||||||
fun createWaiting(
|
fun createWaiting(
|
||||||
@Valid @RequestBody waitingCreateRequest: WaitingCreateRequest,
|
@Valid @RequestBody waitingCreateRequest: WaitingCreateRequest,
|
||||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||||
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>>
|
): ResponseEntity<CommonApiResponse<ReservationCreateResponse>>
|
||||||
|
|
||||||
@LoginRequired
|
@LoginRequired
|
||||||
@Operation(summary = "예약 대기 취소", tags = ["로그인이 필요한 API"])
|
@Operation(summary = "예약 대기 취소", tags = ["로그인이 필요한 API"])
|
||||||
|
|||||||
@ -0,0 +1,94 @@
|
|||||||
|
package roomescape.reservation.implement
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.data.jpa.domain.Specification
|
||||||
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import roomescape.reservation.exception.ReservationErrorCode
|
||||||
|
import roomescape.reservation.exception.ReservationException
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationRepository
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationSearchSpecification
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||||
|
import roomescape.reservation.web.MyReservationRetrieveResponse
|
||||||
|
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||||
|
import roomescape.time.infrastructure.persistence.TimeEntity
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class ReservationFinder(
|
||||||
|
private val reservationRepository: ReservationRepository,
|
||||||
|
private val reservationValidator: ReservationValidator,
|
||||||
|
) {
|
||||||
|
fun findById(id: Long): ReservationEntity {
|
||||||
|
log.debug { "[ReservationFinder.findById] 시작: id=$id" }
|
||||||
|
|
||||||
|
return reservationRepository.findByIdOrNull(id)
|
||||||
|
?.also { log.debug { "[ReservationFinder.findById] 완료: reservationId=$id, date:${it.date}, timeId:${it.time.id}, themeId:${it.theme.id}" } }
|
||||||
|
?: run {
|
||||||
|
log.warn { "[ReservationFinder.findById] 조회 실패: reservationId=$id" }
|
||||||
|
throw ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findAllByStatuses(vararg statuses: ReservationStatus): List<ReservationEntity> {
|
||||||
|
log.debug { "[ReservationFinder.findAll] 시작: status=${statuses}" }
|
||||||
|
|
||||||
|
val spec = ReservationSearchSpecification()
|
||||||
|
.status(*statuses)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return reservationRepository.findAll(spec)
|
||||||
|
.also { log.debug { "[ReservationFinder.findAll] ${it.size}개 예약 조회 완료: status=${statuses}" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findAllByDateAndTheme(
|
||||||
|
date: LocalDate, theme: ThemeEntity
|
||||||
|
): List<ReservationEntity> {
|
||||||
|
log.debug { "[ReservationFinder.findAllByDateAndTheme] 시작: date=$date, themeId=${theme.id}" }
|
||||||
|
|
||||||
|
return reservationRepository.findAllByDateAndTheme(date, theme)
|
||||||
|
.also { log.debug { "[ReservationFinder.findAllByDateAndTheme] ${it.size}개 조회 완료: date=$date, themeId=${theme.id}" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findAllByMemberId(memberId: Long): List<MyReservationRetrieveResponse> {
|
||||||
|
log.debug { "[ReservationFinder.findAllByMemberId] 시작: memberId=${memberId}" }
|
||||||
|
|
||||||
|
return reservationRepository.findAllByMemberId(memberId)
|
||||||
|
.also { log.debug { "[ReservationFinder.findAllByMemberId] ${it.size}개 예약(대기) 조회 완료: memberId=${memberId}" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun searchReservations(
|
||||||
|
themeId: Long?,
|
||||||
|
memberId: Long?,
|
||||||
|
startFrom: LocalDate?,
|
||||||
|
endAt: LocalDate?,
|
||||||
|
): List<ReservationEntity> {
|
||||||
|
reservationValidator.validateSearchDateRange(startFrom, endAt)
|
||||||
|
|
||||||
|
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
||||||
|
.sameThemeId(themeId)
|
||||||
|
.sameMemberId(memberId)
|
||||||
|
.dateStartFrom(startFrom)
|
||||||
|
.dateEndAt(endAt)
|
||||||
|
.status(ReservationStatus.CONFIRMED, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return reservationRepository.findAll(spec)
|
||||||
|
.also {
|
||||||
|
log.debug { "[ReservationFinder.searchReservations] ${it.size}개 예약 조회 완료. " +
|
||||||
|
"themeId=${themeId}, memberId=${memberId}, startFrom=${startFrom}, endAt=${endAt}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isTimeReserved(time: TimeEntity): Boolean {
|
||||||
|
log.debug { "[ReservationFinder.isTimeReserved] 시작: timeId=${time.id}, startAt=${time.startAt}" }
|
||||||
|
|
||||||
|
return reservationRepository.existsByTime(time)
|
||||||
|
.also { log.debug { "[ReservationFinder.isTimeReserved] 완료: isExist=$it, timeId=${time.id}, startAt=${time.startAt}" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,144 @@
|
|||||||
|
package roomescape.reservation.implement
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.data.jpa.domain.Specification
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||||
|
import roomescape.reservation.exception.ReservationErrorCode
|
||||||
|
import roomescape.reservation.exception.ReservationException
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationRepository
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationSearchSpecification
|
||||||
|
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||||
|
import roomescape.time.infrastructure.persistence.TimeEntity
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.LocalTime
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class ReservationValidator(
|
||||||
|
private val reservationRepository: ReservationRepository,
|
||||||
|
) {
|
||||||
|
fun validateIsPast(
|
||||||
|
requestDate: LocalDate,
|
||||||
|
requestTime: LocalTime,
|
||||||
|
) {
|
||||||
|
val now = LocalDateTime.now()
|
||||||
|
val requestDateTime = LocalDateTime.of(requestDate, requestTime)
|
||||||
|
log.debug { "[ReservationValidator.validateIsPast] 시작. request=$requestDateTime, now=$now" }
|
||||||
|
|
||||||
|
if (requestDateTime.isBefore(now)) {
|
||||||
|
log.info { "[ReservationValidator.validateIsPast] 날짜 범위 오류. request=$requestDateTime, now=$now" }
|
||||||
|
throw ReservationException(ReservationErrorCode.PAST_REQUEST_DATETIME)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug { "[ReservationValidator.validateIsPast] 완료. request=$requestDateTime, now=$now" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateSearchDateRange(startFrom: LocalDate?, endAt: LocalDate?) {
|
||||||
|
log.debug { "[ReservationValidator.validateSearchDateRange] 시작: startFrom=$startFrom, endAt=$endAt" }
|
||||||
|
if (startFrom == null || endAt == null) {
|
||||||
|
log.debug { "[ReservationValidator.validateSearchDateRange] 완료: startFrom=$startFrom, endAt=$endAt" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (startFrom.isAfter(endAt)) {
|
||||||
|
log.info { "[ReservationValidator.validateSearchDateRange] 날짜 범위 오류: startFrom=$startFrom, endAt=$endAt" }
|
||||||
|
throw ReservationException(ReservationErrorCode.INVALID_SEARCH_DATE_RANGE)
|
||||||
|
}
|
||||||
|
log.debug { "[ReservationValidator.validateSearchDateRange] 완료: startFrom=$startFrom, endAt=$endAt" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateIsAlreadyExists(date: LocalDate, time: TimeEntity, theme: ThemeEntity) {
|
||||||
|
val themeId = theme.id
|
||||||
|
val timeId = time.id
|
||||||
|
|
||||||
|
log.debug { "[ReservationValidator.validateIsAlreadyExists] 시작: date=$date, timeId=$timeId, themeId=$themeId" }
|
||||||
|
|
||||||
|
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
||||||
|
.sameThemeId(themeId)
|
||||||
|
.sameTimeId(timeId)
|
||||||
|
.sameDate(date)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
if (reservationRepository.exists(spec)) {
|
||||||
|
log.warn { "[ReservationValidator.validateIsAlreadyExists] 중복된 예약 존재: date=$date, timeId=$timeId, themeId=$themeId" }
|
||||||
|
throw ReservationException(ReservationErrorCode.RESERVATION_DUPLICATED)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug { "[ReservationValidator.validateIsAlreadyExists] 완료: date=$date, timeId=$timeId, themeId=$themeId" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateMemberAlreadyReserve(themeId: Long, timeId: Long, date: LocalDate, requesterId: Long) {
|
||||||
|
log.debug { "[ReservationValidator.validateMemberAlreadyReserve] 시작: themeId=$themeId, timeId=$timeId, date=$date, requesterId=$requesterId" }
|
||||||
|
|
||||||
|
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
||||||
|
.sameMemberId(requesterId)
|
||||||
|
.sameThemeId(themeId)
|
||||||
|
.sameTimeId(timeId)
|
||||||
|
.sameDate(date)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
if (reservationRepository.exists(spec)) {
|
||||||
|
log.warn { "[ReservationValidator.validateMemberAlreadyReserve] 중복된 예약 존재: themeId=$themeId, timeId=$timeId, date=$date" }
|
||||||
|
throw ReservationException(ReservationErrorCode.ALREADY_RESERVE)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug { "[ReservationValidator.validateMemberAlreadyReserve] 완료: themeId=$themeId, timeId=$timeId, date=$date, requesterId=$requesterId" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateIsWaiting(reservation: ReservationEntity) {
|
||||||
|
log.debug { "[ReservationValidator.validateIsWaiting] 시작: reservationId=${reservation.id}, status=${reservation.status}" }
|
||||||
|
|
||||||
|
if (!reservation.isWaiting()) {
|
||||||
|
log.warn { "[ReservationValidator.validateIsWaiting] 대기 상태가 아님: reservationId=${reservation.id}, status=${reservation.status}" }
|
||||||
|
throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug { "[ReservationValidator.validateIsWaiting] 완료: reservationId=${reservation.id}, status=${reservation.status}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateCreateAuthority(requester: MemberEntity) {
|
||||||
|
log.debug { "[ReservationValidator.validateCreateAuthority] 시작: requesterId=${requester.id}" }
|
||||||
|
|
||||||
|
if (!requester.isAdmin()) {
|
||||||
|
log.error { "[ReservationValidator.validateCreateAuthority] 관리자가 아닌 다른 회원의 예약 시도: requesterId=${requester.id}" }
|
||||||
|
throw ReservationException(ReservationErrorCode.NO_PERMISSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug { "[ReservationValidator.validateCreateAuthority] 완료: requesterId=${requester.id}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateDeleteAuthority(reservation: ReservationEntity, requester: MemberEntity) {
|
||||||
|
val requesterId: Long = requester.id!!
|
||||||
|
log.debug { "[ReservationValidator.validateDeleteAuthority] 시작: reservationId=${reservation.id}, requesterId=${requesterId}" }
|
||||||
|
|
||||||
|
if (requester.isAdmin()) {
|
||||||
|
log.debug { "[ReservationValidator.validateDeleteAuthority] 완료: reservationId=${reservation.id} requesterId=${requesterId}(Admin)" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reservation.isReservedBy(requesterId)) {
|
||||||
|
log.error {
|
||||||
|
"[ReservationValidator.validateDeleteAuthority] 예약자 본인이 아님: reservationId=${reservation.id}" +
|
||||||
|
", memberId=${reservation.member.id} requesterId=${requesterId} "
|
||||||
|
}
|
||||||
|
throw ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug { "[ReservationValidator.validateDeleteAuthority] 완료: reservationId=${reservation.id}, requesterId=$requesterId" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateAlreadyConfirmed(reservationId: Long) {
|
||||||
|
log.debug { "[ReservationValidator.validateAlreadyConfirmed] 시작: reservationId=$reservationId" }
|
||||||
|
|
||||||
|
if (reservationRepository.isExistConfirmedReservation(reservationId)) {
|
||||||
|
log.warn { "[ReservationWriter.confirm] 이미 확정된 예약: reservationId=$reservationId" }
|
||||||
|
throw ReservationException(ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug { "[ReservationValidator.validateAlreadyConfirmed] 완료: reservationId=$reservationId" }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
package roomescape.reservation.implement
|
||||||
|
|
||||||
|
import com.github.f4b6a3.tsid.TsidFactory
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import roomescape.common.config.next
|
||||||
|
import roomescape.member.implement.MemberFinder
|
||||||
|
import roomescape.reservation.exception.ReservationErrorCode
|
||||||
|
import roomescape.reservation.exception.ReservationException
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationRepository
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||||
|
import roomescape.theme.implement.ThemeFinder
|
||||||
|
import roomescape.time.implement.TimeFinder
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class ReservationWriter(
|
||||||
|
private val reservationValidator: ReservationValidator,
|
||||||
|
private val reservationRepository: ReservationRepository,
|
||||||
|
private val memberFinder: MemberFinder,
|
||||||
|
private val timeFinder: TimeFinder,
|
||||||
|
private val themeFinder: ThemeFinder,
|
||||||
|
private val tsidFactory: TsidFactory,
|
||||||
|
) {
|
||||||
|
fun create(
|
||||||
|
date: LocalDate,
|
||||||
|
timeId: Long,
|
||||||
|
themeId: Long,
|
||||||
|
memberId: Long,
|
||||||
|
status: ReservationStatus,
|
||||||
|
requesterId: Long
|
||||||
|
): ReservationEntity {
|
||||||
|
log.debug {
|
||||||
|
"[ReservationWriter.create] 시작: " +
|
||||||
|
"date=${date}, timeId=${timeId}, themeId=${themeId}, memberId=${memberId}, status=${status}"
|
||||||
|
}
|
||||||
|
val time = timeFinder.findById(timeId).also {
|
||||||
|
reservationValidator.validateIsPast(date, it.startAt)
|
||||||
|
}
|
||||||
|
val theme = themeFinder.findById(themeId)
|
||||||
|
|
||||||
|
val member = memberFinder.findById(memberId).also {
|
||||||
|
if (status == ReservationStatus.WAITING) {
|
||||||
|
reservationValidator.validateMemberAlreadyReserve(themeId, timeId, date, it.id!!)
|
||||||
|
} else {
|
||||||
|
reservationValidator.validateIsAlreadyExists(date, time, theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (memberId != requesterId) {
|
||||||
|
val requester = memberFinder.findById(requesterId)
|
||||||
|
reservationValidator.validateCreateAuthority(requester)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val reservation = ReservationEntity(
|
||||||
|
_id = tsidFactory.next(),
|
||||||
|
date = date,
|
||||||
|
time = time,
|
||||||
|
theme = theme,
|
||||||
|
member = member,
|
||||||
|
status = status
|
||||||
|
)
|
||||||
|
|
||||||
|
return reservationRepository.save(reservation)
|
||||||
|
.also { log.debug { "[ReservationWriter.create] 완료: reservationId=${it.id}, status=${it.status}" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteWaiting(reservation: ReservationEntity, requesterId: Long) {
|
||||||
|
log.debug { "[ReservationWriter.deleteWaiting] 시작: reservationId=${reservation.id}, requesterId=${requesterId}" }
|
||||||
|
|
||||||
|
reservationValidator.validateIsWaiting(reservation)
|
||||||
|
|
||||||
|
delete(reservation, requesterId)
|
||||||
|
.also { log.debug { "[ReservationWriter.deleteWaiting] 완료: reservationId=${reservation.id}, status=${reservation.status}" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteConfirmed(reservation: ReservationEntity, requesterId: Long) {
|
||||||
|
log.debug { "[ReservationWriter.deleteConfirmed] 시작: reservationId=${reservation.id}, requesterId=${requesterId}" }
|
||||||
|
|
||||||
|
delete(reservation, requesterId)
|
||||||
|
.also { log.debug { "[ReservationWriter.deleteConfirmed] 완료: reservationId=${reservation.id}, status=${reservation.status}" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun delete(reservation: ReservationEntity, requesterId: Long) {
|
||||||
|
memberFinder.findById(requesterId)
|
||||||
|
.also { reservationValidator.validateDeleteAuthority(reservation, requester = it) }
|
||||||
|
|
||||||
|
reservationRepository.delete(reservation)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun confirm(reservationId: Long) {
|
||||||
|
log.debug { "[ReservationWriter.confirm] 대기 여부 확인 시작: reservationId=$reservationId" }
|
||||||
|
|
||||||
|
reservationValidator.validateAlreadyConfirmed(reservationId)
|
||||||
|
|
||||||
|
reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)
|
||||||
|
|
||||||
|
log.debug { "[ReservationWriter.confirm] 완료: reservationId=$reservationId, status=${ReservationStatus.CONFIRMED_PAYMENT_REQUIRED}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -48,5 +48,10 @@ class ReservationEntity(
|
|||||||
enum class ReservationStatus {
|
enum class ReservationStatus {
|
||||||
CONFIRMED,
|
CONFIRMED,
|
||||||
CONFIRMED_PAYMENT_REQUIRED,
|
CONFIRMED_PAYMENT_REQUIRED,
|
||||||
WAITING
|
WAITING,
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun confirmedStatus(): Array<ReservationStatus> = arrayOf(CONFIRMED, CONFIRMED_PAYMENT_REQUIRED)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,20 @@
|
|||||||
package roomescape.reservation.infrastructure.persistence
|
package roomescape.reservation.infrastructure.persistence
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.domain.Specification
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor
|
||||||
import org.springframework.data.jpa.repository.Modifying
|
import org.springframework.data.jpa.repository.Modifying
|
||||||
import org.springframework.data.jpa.repository.Query
|
import org.springframework.data.jpa.repository.Query
|
||||||
import org.springframework.data.repository.query.Param
|
import org.springframework.data.repository.query.Param
|
||||||
import roomescape.reservation.web.MyReservationRetrieveResponse
|
import roomescape.reservation.web.MyReservationRetrieveResponse
|
||||||
|
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||||
import roomescape.time.infrastructure.persistence.TimeEntity
|
import roomescape.time.infrastructure.persistence.TimeEntity
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|
||||||
interface ReservationRepository
|
interface ReservationRepository
|
||||||
: JpaRepository<ReservationEntity, Long>, JpaSpecificationExecutor<ReservationEntity> {
|
: JpaRepository<ReservationEntity, Long>, JpaSpecificationExecutor<ReservationEntity> {
|
||||||
fun findAllByTime(time: TimeEntity): List<ReservationEntity>
|
fun findAllByTime(time: TimeEntity): List<ReservationEntity>
|
||||||
|
fun existsByTime(time: TimeEntity): Boolean
|
||||||
|
|
||||||
fun findByDateAndThemeId(date: LocalDate, themeId: Long): List<ReservationEntity>
|
fun findByDateAndThemeId(date: LocalDate, themeId: Long): List<ReservationEntity>
|
||||||
|
|
||||||
@ -20,11 +23,11 @@ interface ReservationRepository
|
|||||||
"""
|
"""
|
||||||
UPDATE ReservationEntity r
|
UPDATE ReservationEntity r
|
||||||
SET r.status = :status
|
SET r.status = :status
|
||||||
WHERE r.id = :id
|
WHERE r._id = :_id
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun updateStatusByReservationId(
|
fun updateStatusByReservationId(
|
||||||
@Param(value = "id") reservationId: Long,
|
@Param(value = "_id") reservationId: Long,
|
||||||
@Param(value = "status") statusForChange: ReservationStatus
|
@Param(value = "status") statusForChange: ReservationStatus
|
||||||
): Int
|
): Int
|
||||||
|
|
||||||
@ -33,28 +36,28 @@ interface ReservationRepository
|
|||||||
SELECT EXISTS (
|
SELECT EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM ReservationEntity r2
|
FROM ReservationEntity r2
|
||||||
WHERE r2.id = :id
|
WHERE r2._id = :_id
|
||||||
AND EXISTS (
|
AND EXISTS (
|
||||||
SELECT 1 FROM ReservationEntity r
|
SELECT 1 FROM ReservationEntity r
|
||||||
WHERE r.theme.id = r2.theme.id
|
WHERE r.theme._id = r2.theme._id
|
||||||
AND r.time.id = r2.time.id
|
AND r.time._id = r2.time._id
|
||||||
AND r.date = r2.date
|
AND r.date = r2.date
|
||||||
AND r.status != 'WAITING'
|
AND r.status != 'WAITING'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun isExistConfirmedReservation(@Param("id") reservationId: Long): Boolean
|
fun isExistConfirmedReservation(@Param("_id") reservationId: Long): Boolean
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT new roomescape.reservation.web.MyReservationRetrieveResponse(
|
SELECT new roomescape.reservation.web.MyReservationRetrieveResponse(
|
||||||
r.id,
|
r._id,
|
||||||
t.name,
|
t.name,
|
||||||
r.date,
|
r.date,
|
||||||
r.time.startAt,
|
r.time.startAt,
|
||||||
r.status,
|
r.status,
|
||||||
(SELECT COUNT (r2) * 1L FROM ReservationEntity r2 WHERE r2.theme = r.theme AND r2.date = r.date AND r2.time = r.time AND r2.id < r.id),
|
(SELECT COUNT (r2) * 1L FROM ReservationEntity r2 WHERE r2.theme = r.theme AND r2.date = r.date AND r2.time = r.time AND r2._id < r._id),
|
||||||
p.paymentKey,
|
p.paymentKey,
|
||||||
p.totalAmount
|
p.totalAmount
|
||||||
)
|
)
|
||||||
@ -62,8 +65,9 @@ interface ReservationRepository
|
|||||||
JOIN r.theme t
|
JOIN r.theme t
|
||||||
LEFT JOIN PaymentEntity p
|
LEFT JOIN PaymentEntity p
|
||||||
ON p.reservation = r
|
ON p.reservation = r
|
||||||
WHERE r.member.id = :memberId
|
WHERE r.member._id = :memberId
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun findAllByMemberId(memberId: Long): List<MyReservationRetrieveResponse>
|
fun findAllByMemberId(memberId: Long): List<MyReservationRetrieveResponse>
|
||||||
|
fun findAllByDateAndTheme(date: LocalDate, theme: ThemeEntity): List<ReservationEntity>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,10 @@ class ReservationSearchSpecification(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
fun status(vararg statuses: ReservationStatus) = andIfNotNull { root, _, cb ->
|
||||||
|
root.get<ReservationStatus>("status").`in`(statuses.toList())
|
||||||
|
}
|
||||||
|
|
||||||
fun confirmed(): ReservationSearchSpecification = andIfNotNull { root, _, cb ->
|
fun confirmed(): ReservationSearchSpecification = andIfNotNull { root, _, cb ->
|
||||||
cb.or(
|
cb.or(
|
||||||
cb.equal(
|
cb.equal(
|
||||||
|
|||||||
@ -10,7 +10,8 @@ import roomescape.payment.infrastructure.client.PaymentApproveRequest
|
|||||||
import roomescape.payment.infrastructure.client.PaymentApproveResponse
|
import roomescape.payment.infrastructure.client.PaymentApproveResponse
|
||||||
import roomescape.payment.infrastructure.client.TossPaymentClient
|
import roomescape.payment.infrastructure.client.TossPaymentClient
|
||||||
import roomescape.payment.web.PaymentCancelRequest
|
import roomescape.payment.web.PaymentCancelRequest
|
||||||
import roomescape.reservation.business.ReservationService
|
import roomescape.reservation.business.ReservationWriteService
|
||||||
|
import roomescape.reservation.business.ReservationFindService
|
||||||
import roomescape.reservation.business.ReservationWithPaymentService
|
import roomescape.reservation.business.ReservationWithPaymentService
|
||||||
import roomescape.reservation.docs.ReservationAPI
|
import roomescape.reservation.docs.ReservationAPI
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
@ -19,12 +20,13 @@ import java.time.LocalDate
|
|||||||
@RestController
|
@RestController
|
||||||
class ReservationController(
|
class ReservationController(
|
||||||
private val reservationWithPaymentService: ReservationWithPaymentService,
|
private val reservationWithPaymentService: ReservationWithPaymentService,
|
||||||
private val reservationService: ReservationService,
|
private val reservationFindService: ReservationFindService,
|
||||||
|
private val reservationWriteService: ReservationWriteService,
|
||||||
private val paymentClient: TossPaymentClient
|
private val paymentClient: TossPaymentClient
|
||||||
) : ReservationAPI {
|
) : ReservationAPI {
|
||||||
@GetMapping("/reservations")
|
@GetMapping("/reservations")
|
||||||
override fun findReservations(): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> {
|
override fun findReservations(): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> {
|
||||||
val response: ReservationRetrieveListResponse = reservationService.findReservations()
|
val response: ReservationRetrieveListResponse = reservationFindService.findReservations()
|
||||||
|
|
||||||
return ResponseEntity.ok(CommonApiResponse(response))
|
return ResponseEntity.ok(CommonApiResponse(response))
|
||||||
}
|
}
|
||||||
@ -33,7 +35,7 @@ class ReservationController(
|
|||||||
override fun findReservationsByMemberId(
|
override fun findReservationsByMemberId(
|
||||||
@MemberId @Parameter(hidden = true) memberId: Long
|
@MemberId @Parameter(hidden = true) memberId: Long
|
||||||
): ResponseEntity<CommonApiResponse<MyReservationRetrieveListResponse>> {
|
): ResponseEntity<CommonApiResponse<MyReservationRetrieveListResponse>> {
|
||||||
val response: MyReservationRetrieveListResponse = reservationService.findReservationsByMemberId(memberId)
|
val response: MyReservationRetrieveListResponse = reservationFindService.findReservationsByMemberId(memberId)
|
||||||
|
|
||||||
return ResponseEntity.ok(CommonApiResponse(response))
|
return ResponseEntity.ok(CommonApiResponse(response))
|
||||||
}
|
}
|
||||||
@ -45,7 +47,7 @@ class ReservationController(
|
|||||||
@RequestParam(required = false) dateFrom: LocalDate?,
|
@RequestParam(required = false) dateFrom: LocalDate?,
|
||||||
@RequestParam(required = false) dateTo: LocalDate?
|
@RequestParam(required = false) dateTo: LocalDate?
|
||||||
): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> {
|
): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> {
|
||||||
val response: ReservationRetrieveListResponse = reservationService.searchReservations(
|
val response: ReservationRetrieveListResponse = reservationFindService.searchReservations(
|
||||||
themeId,
|
themeId,
|
||||||
memberId,
|
memberId,
|
||||||
dateFrom,
|
dateFrom,
|
||||||
@ -61,7 +63,7 @@ class ReservationController(
|
|||||||
@PathVariable("id") reservationId: Long
|
@PathVariable("id") reservationId: Long
|
||||||
): ResponseEntity<CommonApiResponse<Unit>> {
|
): ResponseEntity<CommonApiResponse<Unit>> {
|
||||||
if (reservationWithPaymentService.isNotPaidReservation(reservationId)) {
|
if (reservationWithPaymentService.isNotPaidReservation(reservationId)) {
|
||||||
reservationService.deleteReservation(reservationId, memberId)
|
reservationWriteService.deleteReservation(reservationId, memberId)
|
||||||
return ResponseEntity.noContent().build()
|
return ResponseEntity.noContent().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,19 +81,19 @@ class ReservationController(
|
|||||||
override fun createReservationWithPayment(
|
override fun createReservationWithPayment(
|
||||||
@Valid @RequestBody reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest,
|
@Valid @RequestBody reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest,
|
||||||
@MemberId @Parameter(hidden = true) memberId: Long
|
@MemberId @Parameter(hidden = true) memberId: Long
|
||||||
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> {
|
): ResponseEntity<CommonApiResponse<ReservationCreateResponse>> {
|
||||||
val paymentRequest: PaymentApproveRequest = reservationCreateWithPaymentRequest.toPaymentApproveRequest()
|
val paymentRequest: PaymentApproveRequest = reservationCreateWithPaymentRequest.toPaymentApproveRequest()
|
||||||
val paymentResponse: PaymentApproveResponse = paymentClient.confirm(paymentRequest)
|
val paymentResponse: PaymentApproveResponse = paymentClient.confirm(paymentRequest)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val reservationRetrieveResponse: ReservationRetrieveResponse =
|
val response: ReservationCreateResponse =
|
||||||
reservationWithPaymentService.createReservationAndPayment(
|
reservationWithPaymentService.createReservationAndPayment(
|
||||||
reservationCreateWithPaymentRequest,
|
reservationCreateWithPaymentRequest,
|
||||||
paymentResponse,
|
paymentResponse,
|
||||||
memberId
|
memberId
|
||||||
)
|
)
|
||||||
return ResponseEntity.created(URI.create("/reservations/${reservationRetrieveResponse.id}"))
|
return ResponseEntity.created(URI.create("/reservations/${response.id}"))
|
||||||
.body(CommonApiResponse(reservationRetrieveResponse))
|
.body(CommonApiResponse(response))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val cancelRequest = PaymentCancelRequest(
|
val cancelRequest = PaymentCancelRequest(
|
||||||
paymentRequest.paymentKey,
|
paymentRequest.paymentKey,
|
||||||
@ -110,10 +112,11 @@ class ReservationController(
|
|||||||
|
|
||||||
@PostMapping("/reservations/admin")
|
@PostMapping("/reservations/admin")
|
||||||
override fun createReservationByAdmin(
|
override fun createReservationByAdmin(
|
||||||
@Valid @RequestBody adminReservationRequest: AdminReservationCreateRequest
|
@Valid @RequestBody adminReservationRequest: AdminReservationCreateRequest,
|
||||||
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> {
|
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||||
val response: ReservationRetrieveResponse =
|
): ResponseEntity<CommonApiResponse<ReservationCreateResponse>> {
|
||||||
reservationService.createReservationByAdmin(adminReservationRequest)
|
val response: ReservationCreateResponse =
|
||||||
|
reservationWriteService.createReservationByAdmin(adminReservationRequest, memberId)
|
||||||
|
|
||||||
return ResponseEntity.created(URI.create("/reservations/${response.id}"))
|
return ResponseEntity.created(URI.create("/reservations/${response.id}"))
|
||||||
.body(CommonApiResponse(response))
|
.body(CommonApiResponse(response))
|
||||||
@ -121,7 +124,7 @@ class ReservationController(
|
|||||||
|
|
||||||
@GetMapping("/reservations/waiting")
|
@GetMapping("/reservations/waiting")
|
||||||
override fun findAllWaiting(): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> {
|
override fun findAllWaiting(): ResponseEntity<CommonApiResponse<ReservationRetrieveListResponse>> {
|
||||||
val response: ReservationRetrieveListResponse = reservationService.findAllWaiting()
|
val response: ReservationRetrieveListResponse = reservationFindService.findAllWaiting()
|
||||||
|
|
||||||
return ResponseEntity.ok(CommonApiResponse(response))
|
return ResponseEntity.ok(CommonApiResponse(response))
|
||||||
}
|
}
|
||||||
@ -130,8 +133,8 @@ class ReservationController(
|
|||||||
override fun createWaiting(
|
override fun createWaiting(
|
||||||
@Valid @RequestBody waitingCreateRequest: WaitingCreateRequest,
|
@Valid @RequestBody waitingCreateRequest: WaitingCreateRequest,
|
||||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||||
): ResponseEntity<CommonApiResponse<ReservationRetrieveResponse>> {
|
): ResponseEntity<CommonApiResponse<ReservationCreateResponse>> {
|
||||||
val response: ReservationRetrieveResponse = reservationService.createWaiting(
|
val response: ReservationCreateResponse = reservationWriteService.createWaiting(
|
||||||
waitingCreateRequest,
|
waitingCreateRequest,
|
||||||
memberId
|
memberId
|
||||||
)
|
)
|
||||||
@ -145,7 +148,7 @@ class ReservationController(
|
|||||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||||
@PathVariable("id") reservationId: Long
|
@PathVariable("id") reservationId: Long
|
||||||
): ResponseEntity<CommonApiResponse<Unit>> {
|
): ResponseEntity<CommonApiResponse<Unit>> {
|
||||||
reservationService.deleteWaiting(reservationId, memberId)
|
reservationWriteService.deleteWaiting(reservationId, memberId)
|
||||||
|
|
||||||
return ResponseEntity.noContent().build()
|
return ResponseEntity.noContent().build()
|
||||||
}
|
}
|
||||||
@ -155,7 +158,7 @@ class ReservationController(
|
|||||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||||
@PathVariable("id") reservationId: Long
|
@PathVariable("id") reservationId: Long
|
||||||
): ResponseEntity<CommonApiResponse<Unit>> {
|
): ResponseEntity<CommonApiResponse<Unit>> {
|
||||||
reservationService.confirmWaiting(reservationId, memberId)
|
reservationWriteService.confirmWaiting(reservationId, memberId)
|
||||||
|
|
||||||
return ResponseEntity.ok().build()
|
return ResponseEntity.ok().build()
|
||||||
}
|
}
|
||||||
@ -165,7 +168,7 @@ class ReservationController(
|
|||||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||||
@PathVariable("id") reservationId: Long
|
@PathVariable("id") reservationId: Long
|
||||||
): ResponseEntity<CommonApiResponse<Unit>> {
|
): ResponseEntity<CommonApiResponse<Unit>> {
|
||||||
reservationService.rejectWaiting(reservationId, memberId)
|
reservationWriteService.deleteWaiting(reservationId, memberId)
|
||||||
|
|
||||||
return ResponseEntity.noContent().build()
|
return ResponseEntity.noContent().build()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,12 +7,37 @@ import roomescape.member.web.toRetrieveResponse
|
|||||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||||
import roomescape.theme.web.ThemeRetrieveResponse
|
import roomescape.theme.web.ThemeRetrieveResponse
|
||||||
import roomescape.theme.web.toResponse
|
import roomescape.theme.web.toRetrieveResponse
|
||||||
import roomescape.time.web.TimeCreateResponse
|
import roomescape.time.web.TimeCreateResponse
|
||||||
import roomescape.time.web.toCreateResponse
|
import roomescape.time.web.toCreateResponse
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
|
|
||||||
|
data class ReservationCreateResponse(
|
||||||
|
val id: Long,
|
||||||
|
val date: LocalDate,
|
||||||
|
|
||||||
|
@JsonProperty("member")
|
||||||
|
val member: MemberRetrieveResponse,
|
||||||
|
|
||||||
|
@JsonProperty("time")
|
||||||
|
val time: TimeCreateResponse,
|
||||||
|
|
||||||
|
@JsonProperty("theme")
|
||||||
|
val theme: ThemeRetrieveResponse,
|
||||||
|
|
||||||
|
val status: ReservationStatus
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ReservationEntity.toCreateResponse() = ReservationCreateResponse(
|
||||||
|
id = this.id!!,
|
||||||
|
date = this.date,
|
||||||
|
member = this.member.toRetrieveResponse(),
|
||||||
|
time = this.time.toCreateResponse(),
|
||||||
|
theme = this.theme.toRetrieveResponse(),
|
||||||
|
status = this.status
|
||||||
|
)
|
||||||
|
|
||||||
data class MyReservationRetrieveResponse(
|
data class MyReservationRetrieveResponse(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val themeName: String,
|
val themeName: String,
|
||||||
@ -32,17 +57,19 @@ data class MyReservationRetrieveListResponse(
|
|||||||
val reservations: List<MyReservationRetrieveResponse>
|
val reservations: List<MyReservationRetrieveResponse>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun List<MyReservationRetrieveResponse>.toRetrieveListResponse() = MyReservationRetrieveListResponse(this)
|
||||||
|
|
||||||
data class ReservationRetrieveResponse(
|
data class ReservationRetrieveResponse(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val date: LocalDate,
|
val date: LocalDate,
|
||||||
|
|
||||||
@field:JsonProperty("member")
|
@JsonProperty("member")
|
||||||
val member: MemberRetrieveResponse,
|
val member: MemberRetrieveResponse,
|
||||||
|
|
||||||
@field:JsonProperty("time")
|
@JsonProperty("time")
|
||||||
val time: TimeCreateResponse,
|
val time: TimeCreateResponse,
|
||||||
|
|
||||||
@field:JsonProperty("theme")
|
@JsonProperty("theme")
|
||||||
val theme: ThemeRetrieveResponse,
|
val theme: ThemeRetrieveResponse,
|
||||||
|
|
||||||
val status: ReservationStatus
|
val status: ReservationStatus
|
||||||
@ -53,10 +80,14 @@ fun ReservationEntity.toRetrieveResponse(): ReservationRetrieveResponse = Reserv
|
|||||||
date = this.date,
|
date = this.date,
|
||||||
member = this.member.toRetrieveResponse(),
|
member = this.member.toRetrieveResponse(),
|
||||||
time = this.time.toCreateResponse(),
|
time = this.time.toCreateResponse(),
|
||||||
theme = this.theme.toResponse(),
|
theme = this.theme.toRetrieveResponse(),
|
||||||
status = this.status
|
status = this.status
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ReservationRetrieveListResponse(
|
data class ReservationRetrieveListResponse(
|
||||||
val reservations: List<ReservationRetrieveResponse>
|
val reservations: List<ReservationRetrieveResponse>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun List<ReservationEntity>.toRetrieveListResponse()= ReservationRetrieveListResponse(
|
||||||
|
this.map { it.toRetrieveResponse() }
|
||||||
|
)
|
||||||
|
|||||||
@ -1,92 +1,67 @@
|
|||||||
package roomescape.theme.business
|
package roomescape.theme.business
|
||||||
|
|
||||||
import com.github.f4b6a3.tsid.TsidFactory
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import roomescape.common.config.next
|
import roomescape.theme.implement.ThemeFinder
|
||||||
import roomescape.theme.exception.ThemeErrorCode
|
import roomescape.theme.implement.ThemeWriter
|
||||||
import roomescape.theme.exception.ThemeException
|
|
||||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||||
import roomescape.theme.infrastructure.persistence.ThemeRepository
|
import roomescape.theme.web.*
|
||||||
import roomescape.theme.web.ThemeCreateRequest
|
|
||||||
import roomescape.theme.web.ThemeRetrieveListResponse
|
|
||||||
import roomescape.theme.web.ThemeRetrieveResponse
|
|
||||||
import roomescape.theme.web.toResponse
|
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|
||||||
private val log = KotlinLogging.logger {}
|
private val log = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class ThemeService(
|
class ThemeService(
|
||||||
private val tsidFactory: TsidFactory,
|
private val themeFinder: ThemeFinder,
|
||||||
private val themeRepository: ThemeRepository,
|
private val themeWriter: ThemeWriter,
|
||||||
) {
|
) {
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun findById(id: Long): ThemeEntity {
|
fun findById(id: Long): ThemeEntity {
|
||||||
log.debug { "[ThemeService.findById] 테마 조회 시작: themeId=$id" }
|
log.debug { "[ThemeService.findById] 시작: themeId=$id" }
|
||||||
|
|
||||||
return themeRepository.findByIdOrNull(id)
|
return themeFinder.findById(id)
|
||||||
?.also { log.info { "[ThemeService.findById] 테마 조회 완료: themeId=$id" } }
|
.also { log.info { "[ThemeService.findById] 완료: themeId=$id, name=${it.name}" } }
|
||||||
?: run {
|
|
||||||
log.warn { "[ThemeService.findById] 테마 조회 실패: themeId=$id" }
|
|
||||||
throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun findThemes(): ThemeRetrieveListResponse {
|
fun findThemes(): ThemeRetrieveListResponse {
|
||||||
log.debug { "[ThemeService.findThemes] 모든 테마 조회 시작" }
|
log.debug { "[ThemeService.findThemes] 시작" }
|
||||||
|
|
||||||
return themeRepository.findAll()
|
return themeFinder.findAll()
|
||||||
.also { log.info { "[ThemeService.findThemes] ${it.size}개의 테마 조회 완료" } }
|
.toRetrieveListResponse()
|
||||||
.toResponse()
|
.also { log.info { "[ThemeService.findThemes] 완료. ${it.themes.size}개 반환" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun findMostReservedThemes(count: Int): ThemeRetrieveListResponse {
|
fun findMostReservedThemes(count: Int): ThemeRetrieveListResponse {
|
||||||
log.debug { "[ThemeService.findMostReservedThemes] 인기 테마 조회 시작: count=$count" }
|
log.debug { "[ThemeService.findMostReservedThemes] 시작: count=$count" }
|
||||||
|
|
||||||
val today = LocalDate.now()
|
val today = LocalDate.now()
|
||||||
val startDate = today.minusDays(7)
|
val startFrom = today.minusDays(7)
|
||||||
val endDate = today.minusDays(1)
|
val endAt = today.minusDays(1)
|
||||||
|
|
||||||
return themeRepository.findPopularThemes(startDate, endDate, count)
|
return themeFinder.findMostReservedThemes(count, startFrom, endAt)
|
||||||
.also { log.info { "[ThemeService.findMostReservedThemes] ${it.size} 개의 인기 테마 조회 완료" } }
|
.toRetrieveListResponse()
|
||||||
.toResponse()
|
.also { log.info { "[ThemeService.findMostReservedThemes] ${it.themes.size}개 반환" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun createTheme(request: ThemeCreateRequest): ThemeRetrieveResponse {
|
fun createTheme(request: ThemeCreateRequest): ThemeCreateResponse {
|
||||||
log.debug { "[ThemeService.createTheme] 테마 생성 시작: name=${request.name}" }
|
log.debug { "[ThemeService.createTheme] 시작: name=${request.name}" }
|
||||||
|
|
||||||
if (themeRepository.existsByName(request.name)) {
|
return themeWriter.create(request.name, request.description, request.thumbnail)
|
||||||
log.info { "[ThemeService.createTheme] 테마 생성 실패(이름 중복): name=${request.name}" }
|
.toCreateResponse()
|
||||||
throw ThemeException(ThemeErrorCode.THEME_NAME_DUPLICATED)
|
.also { log.info { "[ThemeService.createTheme] 테마 생성 완료: name=${it.name} themeId=${it.id}" } }
|
||||||
}
|
|
||||||
|
|
||||||
val theme = ThemeEntity(
|
|
||||||
_id = tsidFactory.next(),
|
|
||||||
name = request.name,
|
|
||||||
description = request.description,
|
|
||||||
thumbnail = request.thumbnail
|
|
||||||
)
|
|
||||||
return themeRepository.save(theme)
|
|
||||||
.also { log.info { "[ThemeService.createTheme] 테마 생성 완료: themeId=${it.id}" } }
|
|
||||||
.toResponse()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun deleteTheme(id: Long) {
|
fun deleteTheme(id: Long) {
|
||||||
log.debug { "[ThemeService.deleteTheme] 테마 삭제 시작: themeId=$id" }
|
log.debug { "[ThemeService.deleteTheme] 시작: themeId=$id" }
|
||||||
|
|
||||||
if (themeRepository.isReservedTheme(id)) {
|
val theme: ThemeEntity = themeFinder.findById(id)
|
||||||
log.info { "[ThemeService.deleteTheme] 테마 삭제 실패(예약이 있는 테마): themeId=$id" }
|
|
||||||
throw ThemeException(ThemeErrorCode.THEME_ALREADY_RESERVED)
|
|
||||||
}
|
|
||||||
|
|
||||||
themeRepository.deleteById(id)
|
themeWriter.delete(theme)
|
||||||
.also { log.info { "[ThemeService.deleteTheme] 테마 삭제 완료: themeId=$id" } }
|
.also { log.info { "[ThemeService.deleteTheme] 완료: themeId=$id, name=${theme.name}" } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import roomescape.auth.web.support.Admin
|
|||||||
import roomescape.auth.web.support.LoginRequired
|
import roomescape.auth.web.support.LoginRequired
|
||||||
import roomescape.common.dto.response.CommonApiResponse
|
import roomescape.common.dto.response.CommonApiResponse
|
||||||
import roomescape.theme.web.ThemeCreateRequest
|
import roomescape.theme.web.ThemeCreateRequest
|
||||||
|
import roomescape.theme.web.ThemeCreateResponse
|
||||||
import roomescape.theme.web.ThemeRetrieveListResponse
|
import roomescape.theme.web.ThemeRetrieveListResponse
|
||||||
import roomescape.theme.web.ThemeRetrieveResponse
|
import roomescape.theme.web.ThemeRetrieveResponse
|
||||||
|
|
||||||
@ -38,7 +39,7 @@ interface ThemeAPI {
|
|||||||
)
|
)
|
||||||
fun createTheme(
|
fun createTheme(
|
||||||
@Valid @RequestBody request: ThemeCreateRequest,
|
@Valid @RequestBody request: ThemeCreateRequest,
|
||||||
): ResponseEntity<CommonApiResponse<ThemeRetrieveResponse>>
|
): ResponseEntity<CommonApiResponse<ThemeCreateResponse>>
|
||||||
|
|
||||||
@Admin
|
@Admin
|
||||||
@Operation(summary = "테마 삭제", tags = ["관리자 로그인이 필요한 API"])
|
@Operation(summary = "테마 삭제", tags = ["관리자 로그인이 필요한 API"])
|
||||||
|
|||||||
@ -10,5 +10,6 @@ enum class ThemeErrorCode(
|
|||||||
) : ErrorCode {
|
) : ErrorCode {
|
||||||
THEME_NOT_FOUND(HttpStatus.NOT_FOUND, "TH001", "테마를 찾을 수 없어요."),
|
THEME_NOT_FOUND(HttpStatus.NOT_FOUND, "TH001", "테마를 찾을 수 없어요."),
|
||||||
THEME_NAME_DUPLICATED(HttpStatus.BAD_REQUEST, "TH002", "이미 같은 이름의 테마가 있어요."),
|
THEME_NAME_DUPLICATED(HttpStatus.BAD_REQUEST, "TH002", "이미 같은 이름의 테마가 있어요."),
|
||||||
THEME_ALREADY_RESERVED(HttpStatus.CONFLICT, "TH003", "예약된 테마라 삭제할 수 없어요.")
|
THEME_ALREADY_RESERVED(HttpStatus.CONFLICT, "TH003", "예약된 테마라 삭제할 수 없어요."),
|
||||||
|
INVALID_REQUEST_VALUE(HttpStatus.BAD_REQUEST, "TH004", "입력 값이 잘못되었어요."),
|
||||||
}
|
}
|
||||||
|
|||||||
47
src/main/kotlin/roomescape/theme/implement/ThemeFinder.kt
Normal file
47
src/main/kotlin/roomescape/theme/implement/ThemeFinder.kt
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package roomescape.theme.implement
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import roomescape.theme.exception.ThemeErrorCode
|
||||||
|
import roomescape.theme.exception.ThemeException
|
||||||
|
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||||
|
import roomescape.theme.infrastructure.persistence.ThemeRepository
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class ThemeFinder(
|
||||||
|
private val themeRepository: ThemeRepository
|
||||||
|
) {
|
||||||
|
fun findAll(): List<ThemeEntity> {
|
||||||
|
log.debug { "[ThemeFinder.findAll] 시작" }
|
||||||
|
|
||||||
|
return themeRepository.findAll()
|
||||||
|
.also { log.debug { "[TimeFinder.findAll] ${it.size}개 테마 조회 완료" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findById(id: Long): ThemeEntity {
|
||||||
|
log.debug { "[ThemeFinder.findById] 조회 시작: memberId=$id" }
|
||||||
|
|
||||||
|
return themeRepository.findByIdOrNull(id)
|
||||||
|
?.also { log.debug { "[ThemeFinder.findById] 조회 완료: id=$id, name=${it.name}" } }
|
||||||
|
?: run {
|
||||||
|
log.warn { "[ThemeFinder.findById] 조회 실패: id=$id" }
|
||||||
|
throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findMostReservedThemes(
|
||||||
|
count: Int,
|
||||||
|
startFrom: LocalDate,
|
||||||
|
endAt: LocalDate
|
||||||
|
): List<ThemeEntity> {
|
||||||
|
log.debug { "[ThemeFinder.findMostReservedThemes] 시작. count=$count, startFrom=$startFrom, endAt=$endAt" }
|
||||||
|
|
||||||
|
return themeRepository.findPopularThemes(startFrom, endAt, count)
|
||||||
|
.also { log.debug { "[ThemeFinder.findMostReservedThemes] ${it.size} / ${count}개 테마 조회 완료" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/main/kotlin/roomescape/theme/implement/ThemeValidator.kt
Normal file
43
src/main/kotlin/roomescape/theme/implement/ThemeValidator.kt
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package roomescape.theme.implement
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import roomescape.theme.exception.ThemeErrorCode
|
||||||
|
import roomescape.theme.exception.ThemeException
|
||||||
|
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||||
|
import roomescape.theme.infrastructure.persistence.ThemeRepository
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class ThemeValidator(
|
||||||
|
private val themeRepository: ThemeRepository
|
||||||
|
) {
|
||||||
|
fun validateNameAlreadyExists(name: String) {
|
||||||
|
log.debug { "[ThemeValidator.validateNameAlreadyExists] 시작: name=$name" }
|
||||||
|
|
||||||
|
if (themeRepository.existsByName(name)) {
|
||||||
|
log.info { "[ThemeService.createTheme] 이름 중복: name=${name}" }
|
||||||
|
throw ThemeException(ThemeErrorCode.THEME_NAME_DUPLICATED)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug { "[ThemeValidator.validateNameAlreadyExists] 완료: name=$name" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateIsReserved(theme: ThemeEntity) {
|
||||||
|
val themeId: Long = theme.id ?: run {
|
||||||
|
log.warn { "[ThemeValidator.validateIsReserved] ID를 찾을 수 없음: name:${theme.name}" }
|
||||||
|
throw ThemeException(ThemeErrorCode.INVALID_REQUEST_VALUE)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug { "[ThemeValidator.validateIsReserved] 시작: themeId=${themeId}" }
|
||||||
|
|
||||||
|
if (themeRepository.isReservedTheme(themeId)) {
|
||||||
|
log.info { "[ThemeService.deleteTheme] 예약이 있는 테마: themeId=$themeId" }
|
||||||
|
throw ThemeException(ThemeErrorCode.THEME_ALREADY_RESERVED)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug { "[ThemeValidator.validateIsReserved] 완료: themeId=$themeId" }
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/main/kotlin/roomescape/theme/implement/ThemeWriter.kt
Normal file
41
src/main/kotlin/roomescape/theme/implement/ThemeWriter.kt
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package roomescape.theme.implement
|
||||||
|
|
||||||
|
import com.github.f4b6a3.tsid.TsidFactory
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import roomescape.common.config.next
|
||||||
|
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||||
|
import roomescape.theme.infrastructure.persistence.ThemeRepository
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class ThemeWriter(
|
||||||
|
private val themeValidator: ThemeValidator,
|
||||||
|
private val themeRepository: ThemeRepository,
|
||||||
|
private val tsidFactory: TsidFactory
|
||||||
|
) {
|
||||||
|
fun create(name: String, description: String, thumbnail: String): ThemeEntity {
|
||||||
|
log.debug { "[ThemeWriter.create] 시작: name=$name" }
|
||||||
|
themeValidator.validateNameAlreadyExists(name)
|
||||||
|
|
||||||
|
val theme = ThemeEntity(
|
||||||
|
_id = tsidFactory.next(),
|
||||||
|
name = name,
|
||||||
|
description = description,
|
||||||
|
thumbnail = thumbnail
|
||||||
|
)
|
||||||
|
|
||||||
|
return themeRepository.save(theme)
|
||||||
|
.also { log.debug { "[ThemeWriter.create] 완료: name=$name, id=${it.id}" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delete(theme: ThemeEntity) {
|
||||||
|
log.debug { "[ThemeWriter.delete] 시작: id=${theme.id}" }
|
||||||
|
themeValidator.validateIsReserved(theme)
|
||||||
|
|
||||||
|
themeRepository.delete(theme)
|
||||||
|
.also { log.debug { "[ThemeWriter.delete] 완료: id=${theme.id}, name=${theme.name}" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,27 +6,25 @@ import java.time.LocalDate
|
|||||||
|
|
||||||
interface ThemeRepository : JpaRepository<ThemeEntity, Long> {
|
interface ThemeRepository : JpaRepository<ThemeEntity, Long> {
|
||||||
|
|
||||||
@Query(
|
@Query(value = """
|
||||||
value = """
|
|
||||||
SELECT t
|
SELECT t
|
||||||
FROM ThemeEntity t
|
FROM ThemeEntity t
|
||||||
RIGHT JOIN ReservationEntity r ON t.id = r.theme.id
|
RIGHT JOIN ReservationEntity r ON t._id = r.theme._id
|
||||||
WHERE r.date BETWEEN :startDate AND :endDate
|
WHERE r.date BETWEEN :startFrom AND :endAt
|
||||||
GROUP BY r.theme.id
|
GROUP BY r.theme._id
|
||||||
ORDER BY COUNT(r.theme.id) DESC, t.id ASC
|
ORDER BY COUNT(r.theme._id) DESC, t._id ASC
|
||||||
LIMIT :limit
|
LIMIT :count
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun findPopularThemes(startDate: LocalDate, endDate: LocalDate, limit: Int): List<ThemeEntity>
|
fun findPopularThemes(startFrom: LocalDate, endAt: LocalDate, count: Int): List<ThemeEntity>
|
||||||
|
|
||||||
fun existsByName(name: String): Boolean
|
fun existsByName(name: String): Boolean
|
||||||
|
|
||||||
@Query(
|
@Query(value = """
|
||||||
value = """
|
|
||||||
SELECT EXISTS(
|
SELECT EXISTS(
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM ReservationEntity r
|
FROM ReservationEntity r
|
||||||
WHERE r.theme.id = :id
|
WHERE r.theme._id = :id
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|||||||
@ -33,8 +33,8 @@ class ThemeController(
|
|||||||
@PostMapping("/themes")
|
@PostMapping("/themes")
|
||||||
override fun createTheme(
|
override fun createTheme(
|
||||||
@RequestBody @Valid request: ThemeCreateRequest
|
@RequestBody @Valid request: ThemeCreateRequest
|
||||||
): ResponseEntity<CommonApiResponse<ThemeRetrieveResponse>> {
|
): ResponseEntity<CommonApiResponse<ThemeCreateResponse>> {
|
||||||
val themeResponse: ThemeRetrieveResponse = themeService.createTheme(request)
|
val themeResponse: ThemeCreateResponse = themeService.createTheme(request)
|
||||||
|
|
||||||
return ResponseEntity.created(URI.create("/themes/${themeResponse.id}"))
|
return ResponseEntity.created(URI.create("/themes/${themeResponse.id}"))
|
||||||
.body(CommonApiResponse(themeResponse))
|
.body(CommonApiResponse(themeResponse))
|
||||||
|
|||||||
@ -21,6 +21,21 @@ data class ThemeCreateRequest(
|
|||||||
val thumbnail: String
|
val thumbnail: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class ThemeCreateResponse(
|
||||||
|
val id: Long,
|
||||||
|
val name: String,
|
||||||
|
val description: String,
|
||||||
|
@Schema(description = "썸네일 이미지 주소(URL).")
|
||||||
|
val thumbnail: String
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ThemeEntity.toCreateResponse(): ThemeCreateResponse = ThemeCreateResponse(
|
||||||
|
id = this.id!!,
|
||||||
|
name = this.name,
|
||||||
|
description = this.description,
|
||||||
|
thumbnail = this.thumbnail
|
||||||
|
)
|
||||||
|
|
||||||
data class ThemeRetrieveResponse(
|
data class ThemeRetrieveResponse(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val name: String,
|
val name: String,
|
||||||
@ -29,7 +44,7 @@ data class ThemeRetrieveResponse(
|
|||||||
val thumbnail: String
|
val thumbnail: String
|
||||||
)
|
)
|
||||||
|
|
||||||
fun ThemeEntity.toResponse(): ThemeRetrieveResponse = ThemeRetrieveResponse(
|
fun ThemeEntity.toRetrieveResponse(): ThemeRetrieveResponse = ThemeRetrieveResponse(
|
||||||
id = this.id!!,
|
id = this.id!!,
|
||||||
name = this.name,
|
name = this.name,
|
||||||
description = this.description,
|
description = this.description,
|
||||||
@ -40,6 +55,6 @@ data class ThemeRetrieveListResponse(
|
|||||||
val themes: List<ThemeRetrieveResponse>
|
val themes: List<ThemeRetrieveResponse>
|
||||||
)
|
)
|
||||||
|
|
||||||
fun List<ThemeEntity>.toResponse(): ThemeRetrieveListResponse = ThemeRetrieveListResponse(
|
fun List<ThemeEntity>.toRetrieveListResponse(): ThemeRetrieveListResponse = ThemeRetrieveListResponse(
|
||||||
themes = this.map { it.toResponse() }
|
themes = this.map { it.toRetrieveResponse() }
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,17 +1,11 @@
|
|||||||
package roomescape.time.business
|
package roomescape.time.business
|
||||||
|
|
||||||
import com.github.f4b6a3.tsid.TsidFactory
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import roomescape.common.config.next
|
import roomescape.time.implement.TimeFinder
|
||||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
import roomescape.time.implement.TimeWriter
|
||||||
import roomescape.reservation.infrastructure.persistence.ReservationRepository
|
|
||||||
import roomescape.time.exception.TimeErrorCode
|
|
||||||
import roomescape.time.exception.TimeException
|
|
||||||
import roomescape.time.infrastructure.persistence.TimeEntity
|
import roomescape.time.infrastructure.persistence.TimeEntity
|
||||||
import roomescape.time.infrastructure.persistence.TimeRepository
|
|
||||||
import roomescape.time.web.*
|
import roomescape.time.web.*
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
@ -20,87 +14,61 @@ private val log = KotlinLogging.logger {}
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
class TimeService(
|
class TimeService(
|
||||||
private val tsidFactory: TsidFactory,
|
private val timeFinder: TimeFinder,
|
||||||
private val timeRepository: TimeRepository,
|
private val timeWriter: TimeWriter,
|
||||||
private val reservationRepository: ReservationRepository,
|
|
||||||
) {
|
) {
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun findById(id: Long): TimeEntity {
|
fun findById(id: Long): TimeEntity {
|
||||||
log.debug { "[TimeService.findById] 시간 조회 시작: timeId=$id" }
|
log.debug { "[TimeService.findById] 시작: timeId=$id" }
|
||||||
return timeRepository.findByIdOrNull(id)
|
|
||||||
?.also { log.info { "[TimeService.findById] 시간 조회 완료: timeId=$id" } }
|
return timeFinder.findById(id)
|
||||||
?: run {
|
.also { log.info { "[TimeService.findById] 완료: timeId=$id, startAt=${it.startAt}" } }
|
||||||
log.warn { "[TimeService.findById] 시간 조회 실패: timeId=$id" }
|
|
||||||
throw TimeException(TimeErrorCode.TIME_NOT_FOUND)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun findTimes(): TimeRetrieveListResponse {
|
fun findTimes(): TimeRetrieveListResponse {
|
||||||
log.debug { "[TimeService.findTimes] 모든 시간 조회 시작" }
|
log.debug { "[TimeService.findTimes] 시작" }
|
||||||
return timeRepository.findAll()
|
|
||||||
.also { log.info { "[TimeService.findTimes] ${it.size}개의 시간 조회 완료" } }
|
return timeFinder.findAll()
|
||||||
.toResponse()
|
.toResponse()
|
||||||
}
|
.also { log.info { "[TimeService.findTimes] 완료. ${it.times.size}개 반환" } }
|
||||||
|
|
||||||
@Transactional
|
|
||||||
fun createTime(request: TimeCreateRequest): TimeCreateResponse {
|
|
||||||
log.debug { "[TimeService.createTime] 시간 생성 시작: startAt=${request.startAt}" }
|
|
||||||
|
|
||||||
val startAt: LocalTime = request.startAt
|
|
||||||
if (timeRepository.existsByStartAt(startAt)) {
|
|
||||||
log.info { "[TimeService.createTime] 시간 생성 실패(시간 중복): startAt=$startAt" }
|
|
||||||
throw TimeException(TimeErrorCode.TIME_DUPLICATED)
|
|
||||||
}
|
|
||||||
|
|
||||||
val time = TimeEntity(
|
|
||||||
_id = tsidFactory.next(),
|
|
||||||
startAt = request.startAt
|
|
||||||
)
|
|
||||||
return timeRepository.save(time)
|
|
||||||
.also { log.info { "[TimeService.createTime] 시간 생성 완료: timeId=${it.id}" } }
|
|
||||||
.toCreateResponse()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
fun deleteTime(id: Long) {
|
|
||||||
log.debug { "[TimeService.deleteTime] 시간 삭제 시작: timeId=$id" }
|
|
||||||
|
|
||||||
val time: TimeEntity = findById(id)
|
|
||||||
|
|
||||||
log.debug { "[TimeService.deleteTime] 시간이 ${time.startAt}인 모든 예약 조회 시작" }
|
|
||||||
val reservations: List<ReservationEntity> = reservationRepository.findAllByTime(time)
|
|
||||||
log.debug { "[TimeService.deleteTime] 시간이 ${time.startAt}인 모든 ${reservations.size} 개의 예약 조회 완료" }
|
|
||||||
|
|
||||||
if (reservations.isNotEmpty()) {
|
|
||||||
log.info { "[TimeService.deleteTime] 시간 삭제 실패(예약이 있는 시간): timeId=$id" }
|
|
||||||
throw TimeException(TimeErrorCode.TIME_ALREADY_RESERVED)
|
|
||||||
}
|
|
||||||
|
|
||||||
timeRepository.delete(time)
|
|
||||||
.also { log.info { "[TimeService.deleteTime] 시간 삭제 완료: timeId=$id" } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun findTimesWithAvailability(date: LocalDate, themeId: Long): TimeWithAvailabilityListResponse {
|
fun findTimesWithAvailability(date: LocalDate, themeId: Long): TimeWithAvailabilityListResponse {
|
||||||
log.debug { "[TimeService.findTimesWithAvailability] 예약 가능 시간 조회 시작: date=$date, themeId=$themeId" }
|
log.debug { "[TimeService.findTimesWithAvailability] 시작: date=$date, themeId=$themeId" }
|
||||||
|
|
||||||
log.debug { "[TimeService.findTimesWithAvailability] 모든 시간 조회 " }
|
val times: List<TimeWithAvailabilityResponse> =
|
||||||
val allTimes = timeRepository.findAll()
|
timeFinder.findAllWithAvailabilityByDateAndThemeId(date, themeId)
|
||||||
log.debug { "[TimeService.findTimesWithAvailability] ${allTimes.size}개의 시간 조회 완료" }
|
.map {
|
||||||
|
TimeWithAvailabilityResponse(
|
||||||
|
id = it.timeId,
|
||||||
|
startAt = it.startAt,
|
||||||
|
isAvailable = it.isReservable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
log.debug { "[TimeService.findTimesWithAvailability] date=$date, themeId=$themeId 인 모든 예약 조회 시작" }
|
return TimeWithAvailabilityListResponse(times)
|
||||||
val reservations: List<ReservationEntity> = reservationRepository.findByDateAndThemeId(date, themeId)
|
.also { log.info { "[TimeService.findTimesWithAvailability] ${it.times.size}개 반환: date=$date, themeId=$themeId" } }
|
||||||
log.debug { "[TimeService.findTimesWithAvailability] date=$date, themeId=$themeId 인 ${reservations.size} 개의 예약 조회 완료" }
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun createTime(request: TimeCreateRequest): TimeCreateResponse {
|
||||||
|
val startAt: LocalTime = request.startAt
|
||||||
|
log.debug { "[TimeService.createTime] 시작: startAt=${startAt}" }
|
||||||
|
|
||||||
return TimeWithAvailabilityListResponse(allTimes.map { time ->
|
return timeWriter.create(startAt)
|
||||||
val isAvailable: Boolean = reservations.none { reservation -> reservation.time.id == time.id }
|
.toCreateResponse()
|
||||||
TimeWithAvailabilityResponse(time.id!!, time.startAt, isAvailable)
|
.also { log.info { "[TimeService.createTime] 완료: startAt=${startAt}, timeId=${it.id}" } }
|
||||||
}).also {
|
}
|
||||||
log.info {
|
|
||||||
"[TimeService.findTimesWithAvailability] date=$date, themeId=$themeId 에 대한 예약 가능 여부가 담긴 모든 시간 조회 완료"
|
@Transactional
|
||||||
}
|
fun deleteTime(id: Long) {
|
||||||
}
|
log.debug { "[TimeService.deleteTime] 시작: timeId=$id" }
|
||||||
|
|
||||||
|
val time: TimeEntity = timeFinder.findById(id)
|
||||||
|
|
||||||
|
timeWriter.delete(time)
|
||||||
|
.also { log.info { "[TimeService.deleteTime] 완료: timeId=$id, startAt=${time.startAt}" } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
package roomescape.time.business.domain
|
||||||
|
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalTime
|
||||||
|
|
||||||
|
class TimeWithAvailability(
|
||||||
|
val timeId: Long,
|
||||||
|
val startAt: LocalTime,
|
||||||
|
val date: LocalDate,
|
||||||
|
val themeId: Long,
|
||||||
|
val isReservable: Boolean
|
||||||
|
)
|
||||||
60
src/main/kotlin/roomescape/time/implement/TimeFinder.kt
Normal file
60
src/main/kotlin/roomescape/time/implement/TimeFinder.kt
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package roomescape.time.implement
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import roomescape.reservation.implement.ReservationFinder
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||||
|
import roomescape.time.business.domain.TimeWithAvailability
|
||||||
|
import roomescape.theme.implement.ThemeFinder
|
||||||
|
import roomescape.time.exception.TimeErrorCode
|
||||||
|
import roomescape.time.exception.TimeException
|
||||||
|
import roomescape.time.infrastructure.persistence.TimeEntity
|
||||||
|
import roomescape.time.infrastructure.persistence.TimeRepository
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class TimeFinder(
|
||||||
|
private val timeRepository: TimeRepository,
|
||||||
|
private val reservationFinder: ReservationFinder,
|
||||||
|
private val themeFinder: ThemeFinder
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun findAll(): List<TimeEntity> {
|
||||||
|
log.debug { "[TimeFinder.findAll] 시작" }
|
||||||
|
|
||||||
|
return timeRepository.findAll()
|
||||||
|
.also { log.debug { "[TimeFinder.findAll] ${it.size}개 시간 조회 완료" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findById(id: Long): TimeEntity {
|
||||||
|
log.debug { "[TimeFinder.findById] 조회 시작: timeId=$id" }
|
||||||
|
|
||||||
|
return timeRepository.findByIdOrNull(id)
|
||||||
|
?.also { log.debug { "[TimeFinder.findById] 조회 완료: timeId=$id, startAt=${it.startAt}" } }
|
||||||
|
?: run {
|
||||||
|
log.warn { "[TimeFinder.findById] 조회 실패: timeId=$id" }
|
||||||
|
throw TimeException(TimeErrorCode.TIME_NOT_FOUND)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findAllWithAvailabilityByDateAndThemeId(
|
||||||
|
date: LocalDate, themeId: Long
|
||||||
|
): List<TimeWithAvailability> {
|
||||||
|
log.debug { "[TimeFinder.findAllWithAvailabilityByDateAndThemeId] 조회 시작: date:$date, themeId=$themeId" }
|
||||||
|
|
||||||
|
val theme = themeFinder.findById(themeId)
|
||||||
|
val reservations: List<ReservationEntity> = reservationFinder.findAllByDateAndTheme(date, theme)
|
||||||
|
val allTimes: List<TimeEntity> = findAll()
|
||||||
|
|
||||||
|
return allTimes.map { time ->
|
||||||
|
val isReservable: Boolean = reservations.any { reservation -> time.id == reservation.id }
|
||||||
|
TimeWithAvailability(time.id!!, time.startAt, date, themeId, isReservable)
|
||||||
|
}.also {
|
||||||
|
log.debug { "[TimeFinder.findAllWithAvailabilityByDateAndThemeId] ${it.size}개 조회 완료: date:$date, themeId=$themeId" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/main/kotlin/roomescape/time/implement/TimeValidator.kt
Normal file
41
src/main/kotlin/roomescape/time/implement/TimeValidator.kt
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package roomescape.time.implement
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import roomescape.reservation.implement.ReservationFinder
|
||||||
|
import roomescape.time.exception.TimeErrorCode
|
||||||
|
import roomescape.time.exception.TimeException
|
||||||
|
import roomescape.time.infrastructure.persistence.TimeEntity
|
||||||
|
import roomescape.time.infrastructure.persistence.TimeRepository
|
||||||
|
import java.time.LocalTime
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class TimeValidator(
|
||||||
|
private val timeRepository: TimeRepository,
|
||||||
|
private val reservationFinder: ReservationFinder
|
||||||
|
) {
|
||||||
|
fun validateIsAlreadyExists(startAt: LocalTime) {
|
||||||
|
log.debug { "[TimeValidator.validateIsAlreadyExists] 시작: startAt=${startAt}" }
|
||||||
|
|
||||||
|
if (timeRepository.existsByStartAt(startAt)) {
|
||||||
|
log.info { "[TimeValidator.validateIsAlreadyExists] 중복 시간: startAt=$startAt" }
|
||||||
|
throw TimeException(TimeErrorCode.TIME_DUPLICATED)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug { "[TimeValidator.validateIsAlreadyExists] 완료: startAt=${startAt}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validateIsReserved(time: TimeEntity) {
|
||||||
|
log.debug { "[TimeValidator.validateIsReserved] 시작: id=${time.id}, startAt=${time.startAt}" }
|
||||||
|
|
||||||
|
if (reservationFinder.isTimeReserved(time)) {
|
||||||
|
log.info { "[TimeValidator.validateIsReserved] 예약이 있는 시간: timeId=${time.id}, startAt=${time.startAt}" }
|
||||||
|
throw TimeException(TimeErrorCode.TIME_ALREADY_RESERVED)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug { "[TimeValidator.validateIsReserved] 시작: id=${time.id}, startAt=${time.startAt}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/main/kotlin/roomescape/time/implement/TimeWriter.kt
Normal file
37
src/main/kotlin/roomescape/time/implement/TimeWriter.kt
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package roomescape.time.implement
|
||||||
|
|
||||||
|
import com.github.f4b6a3.tsid.TsidFactory
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import roomescape.common.config.next
|
||||||
|
import roomescape.time.infrastructure.persistence.TimeEntity
|
||||||
|
import roomescape.time.infrastructure.persistence.TimeRepository
|
||||||
|
import java.time.LocalTime
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class TimeWriter(
|
||||||
|
private val timeValidator: TimeValidator,
|
||||||
|
private val timeRepository: TimeRepository,
|
||||||
|
private val tsidFactory: TsidFactory
|
||||||
|
) {
|
||||||
|
fun create(startAt: LocalTime): TimeEntity {
|
||||||
|
log.debug { "[TimeWriter.create] 시작: startAt=$startAt" }
|
||||||
|
timeValidator.validateIsAlreadyExists(startAt)
|
||||||
|
|
||||||
|
val time = TimeEntity(_id = tsidFactory.next(), startAt = startAt)
|
||||||
|
|
||||||
|
return timeRepository.save(time)
|
||||||
|
.also { log.debug { "[TimeWriter.create] 완료: startAt=$startAt, id=${it.id}" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delete(time: TimeEntity) {
|
||||||
|
log.debug { "[TimeWriter.delete] 시작: id=${time.id}" }
|
||||||
|
timeValidator.validateIsReserved(time)
|
||||||
|
|
||||||
|
timeRepository.delete(time)
|
||||||
|
.also { log.debug { "[TimeWriter.delete] 완료: id=${time.id}, startAt=${time.startAt}" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,23 +6,21 @@ import io.kotest.core.spec.style.BehaviorSpec
|
|||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
|
||||||
import roomescape.auth.exception.AuthErrorCode
|
import roomescape.auth.exception.AuthErrorCode
|
||||||
import roomescape.auth.exception.AuthException
|
import roomescape.auth.exception.AuthException
|
||||||
import roomescape.auth.infrastructure.jwt.JwtHandler
|
import roomescape.auth.infrastructure.jwt.JwtHandler
|
||||||
import roomescape.member.business.MemberService
|
import roomescape.member.exception.MemberErrorCode
|
||||||
|
import roomescape.member.exception.MemberException
|
||||||
|
import roomescape.member.implement.MemberFinder
|
||||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||||
import roomescape.member.infrastructure.persistence.MemberRepository
|
|
||||||
import roomescape.util.JwtFixture
|
import roomescape.util.JwtFixture
|
||||||
import roomescape.util.MemberFixture
|
import roomescape.util.MemberFixture
|
||||||
import roomescape.util.TsidFactory
|
|
||||||
|
|
||||||
class AuthServiceTest : BehaviorSpec({
|
class AuthServiceTest : BehaviorSpec({
|
||||||
val memberRepository: MemberRepository = mockk()
|
val memberFinder: MemberFinder = mockk()
|
||||||
val memberService = MemberService(TsidFactory, memberRepository)
|
|
||||||
val jwtHandler: JwtHandler = JwtFixture.create()
|
val jwtHandler: JwtHandler = JwtFixture.create()
|
||||||
|
|
||||||
val authService = AuthService(memberService, jwtHandler)
|
val authService = AuthService(memberFinder, jwtHandler)
|
||||||
val user: MemberEntity = MemberFixture.user()
|
val user: MemberEntity = MemberFixture.user()
|
||||||
|
|
||||||
Given("로그인 요청을 받으면") {
|
Given("로그인 요청을 받으면") {
|
||||||
@ -31,7 +29,7 @@ class AuthServiceTest : BehaviorSpec({
|
|||||||
|
|
||||||
Then("회원이 있다면 JWT 토큰을 생성한 뒤 반환한다.") {
|
Then("회원이 있다면 JWT 토큰을 생성한 뒤 반환한다.") {
|
||||||
every {
|
every {
|
||||||
memberRepository.findByEmailAndPassword(request.email, request.password)
|
memberFinder.findByEmailAndPassword(request.email, request.password)
|
||||||
} returns user
|
} returns user
|
||||||
|
|
||||||
val accessToken: String = authService.login(request).accessToken
|
val accessToken: String = authService.login(request).accessToken
|
||||||
@ -42,8 +40,8 @@ class AuthServiceTest : BehaviorSpec({
|
|||||||
|
|
||||||
Then("회원이 없다면 예외를 던진다.") {
|
Then("회원이 없다면 예외를 던진다.") {
|
||||||
every {
|
every {
|
||||||
memberRepository.findByEmailAndPassword(request.email, request.password)
|
memberFinder.findByEmailAndPassword(request.email, request.password)
|
||||||
} returns null
|
} throws MemberException(MemberErrorCode.MEMBER_NOT_FOUND)
|
||||||
|
|
||||||
val exception = shouldThrow<AuthException> {
|
val exception = shouldThrow<AuthException> {
|
||||||
authService.login(request)
|
authService.login(request)
|
||||||
@ -59,7 +57,7 @@ class AuthServiceTest : BehaviorSpec({
|
|||||||
val userId: Long = user.id!!
|
val userId: Long = user.id!!
|
||||||
|
|
||||||
Then("회원이 있다면 회원의 이름을 반환한다.") {
|
Then("회원이 있다면 회원의 이름을 반환한다.") {
|
||||||
every { memberRepository.findByIdOrNull(userId) } returns user
|
every { memberFinder.findById(userId) } returns user
|
||||||
|
|
||||||
val response = authService.checkLogin(userId)
|
val response = authService.checkLogin(userId)
|
||||||
|
|
||||||
@ -69,7 +67,9 @@ class AuthServiceTest : BehaviorSpec({
|
|||||||
}
|
}
|
||||||
|
|
||||||
Then("회원이 없다면 예외를 던진다.") {
|
Then("회원이 없다면 예외를 던진다.") {
|
||||||
every { memberRepository.findByIdOrNull(userId) } returns null
|
every {
|
||||||
|
memberFinder.findById(userId)
|
||||||
|
} throws MemberException(MemberErrorCode.MEMBER_NOT_FOUND)
|
||||||
|
|
||||||
val exception = shouldThrow<AuthException> {
|
val exception = shouldThrow<AuthException> {
|
||||||
authService.checkLogin(userId)
|
authService.checkLogin(userId)
|
||||||
|
|||||||
@ -25,14 +25,12 @@ class JwtHandlerTest : FunSpec({
|
|||||||
}
|
}
|
||||||
|
|
||||||
test("만료된 토큰이면 예외를 던진다.") {
|
test("만료된 토큰이면 예외를 던진다.") {
|
||||||
// given
|
|
||||||
val expirationTime = 0L
|
val expirationTime = 0L
|
||||||
val shortExpirationTimeJwtHandler: JwtHandler = JwtFixture.create(expirationTime = expirationTime)
|
val shortExpirationTimeJwtHandler: JwtHandler = JwtFixture.create(expirationTime = expirationTime)
|
||||||
val token = shortExpirationTimeJwtHandler.createToken(memberId)
|
val token = shortExpirationTimeJwtHandler.createToken(memberId)
|
||||||
|
|
||||||
Thread.sleep(expirationTime) // 만료 시간 이후로 대기
|
Thread.sleep(expirationTime) // 만료 시간 이후로 대기
|
||||||
|
|
||||||
// when & then
|
|
||||||
shouldThrow<AuthException> {
|
shouldThrow<AuthException> {
|
||||||
shortExpirationTimeJwtHandler.getMemberIdFromToken(token)
|
shortExpirationTimeJwtHandler.getMemberIdFromToken(token)
|
||||||
}.errorCode shouldBe AuthErrorCode.EXPIRED_TOKEN
|
}.errorCode shouldBe AuthErrorCode.EXPIRED_TOKEN
|
||||||
|
|||||||
@ -1,24 +1,24 @@
|
|||||||
package roomescape.auth.web
|
package roomescape.auth.web
|
||||||
|
|
||||||
import com.ninjasquad.springmockk.SpykBean
|
import com.ninjasquad.springmockk.MockkBean
|
||||||
|
import io.mockk.Runs
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
import org.hamcrest.Matchers.equalTo
|
import org.hamcrest.Matchers.equalTo
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import roomescape.auth.business.AuthService
|
import roomescape.auth.business.AuthService
|
||||||
import roomescape.auth.exception.AuthErrorCode
|
import roomescape.auth.exception.AuthErrorCode
|
||||||
|
import roomescape.auth.exception.AuthException
|
||||||
import roomescape.common.exception.CommonErrorCode
|
import roomescape.common.exception.CommonErrorCode
|
||||||
import roomescape.common.exception.ErrorCode
|
import roomescape.common.exception.ErrorCode
|
||||||
import roomescape.util.MemberFixture
|
import roomescape.util.MemberFixture
|
||||||
import roomescape.util.RoomescapeApiTest
|
import roomescape.util.RoomescapeApiTest
|
||||||
|
|
||||||
@WebMvcTest(controllers = [AuthController::class])
|
@WebMvcTest(controllers = [AuthController::class])
|
||||||
class AuthControllerTest(
|
class AuthControllerTest(val mockMvc: MockMvc) : RoomescapeApiTest() {
|
||||||
val mockMvc: MockMvc
|
|
||||||
) : RoomescapeApiTest() {
|
|
||||||
|
|
||||||
@SpykBean
|
@MockkBean
|
||||||
private lateinit var authService: AuthService
|
private lateinit var authService: AuthService
|
||||||
|
|
||||||
val userRequest: LoginRequest = MemberFixture.userLoginRequest()
|
val userRequest: LoginRequest = MemberFixture.userLoginRequest()
|
||||||
@ -31,12 +31,8 @@ class AuthControllerTest(
|
|||||||
val expectedToken = "expectedToken"
|
val expectedToken = "expectedToken"
|
||||||
|
|
||||||
every {
|
every {
|
||||||
memberRepository.findByEmailAndPassword(userRequest.email, userRequest.password)
|
authService.login(userRequest)
|
||||||
} returns user
|
} returns LoginResponse(expectedToken)
|
||||||
|
|
||||||
every {
|
|
||||||
jwtHandler.createToken(user.id!!)
|
|
||||||
} returns expectedToken
|
|
||||||
|
|
||||||
Then("토큰을 반환한다.") {
|
Then("토큰을 반환한다.") {
|
||||||
runPostTest(
|
runPostTest(
|
||||||
@ -51,12 +47,13 @@ class AuthControllerTest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
When("회원을 찾지 못하면") {
|
When("회원을 찾지 못하면") {
|
||||||
|
val expectedError = AuthErrorCode.LOGIN_FAILED
|
||||||
|
|
||||||
every {
|
every {
|
||||||
memberRepository.findByEmailAndPassword(userRequest.email, userRequest.password)
|
authService.login(userRequest)
|
||||||
} returns null
|
} throws AuthException(expectedError)
|
||||||
|
|
||||||
Then("에러 응답을 받는다.") {
|
Then("에러 응답을 받는다.") {
|
||||||
val expectedError = AuthErrorCode.LOGIN_FAILED
|
|
||||||
runPostTest(
|
runPostTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
endpoint = endpoint,
|
endpoint = endpoint,
|
||||||
@ -67,6 +64,7 @@ class AuthControllerTest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
When("입력 값이 잘못되면") {
|
When("입력 값이 잘못되면") {
|
||||||
val expectedErrorCode: ErrorCode = CommonErrorCode.INVALID_INPUT_VALUE
|
val expectedErrorCode: ErrorCode = CommonErrorCode.INVALID_INPUT_VALUE
|
||||||
|
|
||||||
@ -96,6 +94,10 @@ class AuthControllerTest(
|
|||||||
loginAsUser()
|
loginAsUser()
|
||||||
|
|
||||||
Then("회원의 이름과 권한을 응답한다") {
|
Then("회원의 이름과 권한을 응답한다") {
|
||||||
|
every {
|
||||||
|
authService.checkLogin(user.id!!)
|
||||||
|
} returns LoginCheckResponse(user.name, user.role.name)
|
||||||
|
|
||||||
runGetTest(
|
runGetTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
endpoint = endpoint,
|
endpoint = endpoint,
|
||||||
@ -109,12 +111,12 @@ class AuthControllerTest(
|
|||||||
|
|
||||||
When("토큰은 있지만 회원을 찾을 수 없으면") {
|
When("토큰은 있지만 회원을 찾을 수 없으면") {
|
||||||
val invalidMemberId: Long = -1L
|
val invalidMemberId: Long = -1L
|
||||||
|
val expectedError = AuthErrorCode.MEMBER_NOT_FOUND
|
||||||
|
|
||||||
every { jwtHandler.getMemberIdFromToken(any()) } returns invalidMemberId
|
every { jwtHandler.getMemberIdFromToken(any()) } returns invalidMemberId
|
||||||
every { memberRepository.findByIdOrNull(invalidMemberId) } returns null
|
every { authService.checkLogin(invalidMemberId) } throws AuthException(expectedError)
|
||||||
|
|
||||||
Then("에러 응답을 받는다.") {
|
Then("에러 응답을 받는다.") {
|
||||||
val expectedError = AuthErrorCode.MEMBER_NOT_FOUND
|
|
||||||
runGetTest(
|
runGetTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
endpoint = endpoint,
|
endpoint = endpoint,
|
||||||
@ -129,13 +131,11 @@ class AuthControllerTest(
|
|||||||
val endpoint = "/logout"
|
val endpoint = "/logout"
|
||||||
|
|
||||||
When("토큰으로 memberId 조회가 가능하면") {
|
When("토큰으로 memberId 조회가 가능하면") {
|
||||||
every {
|
loginAsUser()
|
||||||
jwtHandler.getMemberIdFromToken(any())
|
|
||||||
} returns 1L
|
|
||||||
|
|
||||||
every {
|
every {
|
||||||
memberRepository.findByIdOrNull(1L)
|
authService.logout(user.id!!)
|
||||||
} returns MemberFixture.create(id = 1L)
|
} just Runs
|
||||||
|
|
||||||
Then("정상 응답한다.") {
|
Then("정상 응답한다.") {
|
||||||
runPostTest(
|
runPostTest(
|
||||||
|
|||||||
100
src/test/kotlin/roomescape/member/business/MemberServiceTest.kt
Normal file
100
src/test/kotlin/roomescape/member/business/MemberServiceTest.kt
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package roomescape.member.business
|
||||||
|
|
||||||
|
import io.kotest.assertions.assertSoftly
|
||||||
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.collections.shouldContainExactly
|
||||||
|
import io.kotest.matchers.collections.shouldHaveSize
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import roomescape.member.exception.MemberErrorCode
|
||||||
|
import roomescape.member.exception.MemberException
|
||||||
|
import roomescape.member.implement.MemberFinder
|
||||||
|
import roomescape.member.implement.MemberWriter
|
||||||
|
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||||
|
import roomescape.member.infrastructure.persistence.Role
|
||||||
|
import roomescape.member.web.SignupRequest
|
||||||
|
import roomescape.util.MemberFixture
|
||||||
|
|
||||||
|
class MemberServiceTest : FunSpec({
|
||||||
|
val memberWriter: MemberWriter = mockk()
|
||||||
|
val memberFinder: MemberFinder = mockk()
|
||||||
|
|
||||||
|
val memberService = MemberService(memberWriter, memberFinder)
|
||||||
|
|
||||||
|
context("findMembers") {
|
||||||
|
test("정상 응답") {
|
||||||
|
val members: List<MemberEntity> = listOf(
|
||||||
|
MemberFixture.create(name = "user1"),
|
||||||
|
MemberFixture.create(name = "user2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
every { memberFinder.findAll() } returns members
|
||||||
|
|
||||||
|
val response = memberService.findMembers()
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertSoftly(response.members) {
|
||||||
|
it shouldHaveSize 2
|
||||||
|
it.map { member -> member.name } shouldContainExactly listOf("user1", "user2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("findById") {
|
||||||
|
val id = 1L
|
||||||
|
|
||||||
|
test("정상 응답") {
|
||||||
|
every {
|
||||||
|
memberFinder.findById(id)
|
||||||
|
} returns MemberFixture.create(id = id)
|
||||||
|
|
||||||
|
memberService.findById(id).id shouldBe id
|
||||||
|
}
|
||||||
|
|
||||||
|
test("회원을 찾을 수 없으면 예외 응답") {
|
||||||
|
every {
|
||||||
|
memberFinder.findById(id)
|
||||||
|
} throws MemberException(MemberErrorCode.MEMBER_NOT_FOUND)
|
||||||
|
|
||||||
|
shouldThrow<MemberException> {
|
||||||
|
memberService.findById(id)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe MemberErrorCode.MEMBER_NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("createMember") {
|
||||||
|
val request = SignupRequest(name = "new-user", email = "new@test.com", password = "password")
|
||||||
|
|
||||||
|
test("정상 저장") {
|
||||||
|
val member = MemberFixture.create(
|
||||||
|
name = request.name,
|
||||||
|
account = request.email,
|
||||||
|
password = request.password
|
||||||
|
)
|
||||||
|
|
||||||
|
every {
|
||||||
|
memberWriter.create(request.name, request.email, request.password, Role.MEMBER)
|
||||||
|
} returns member
|
||||||
|
|
||||||
|
val response = memberService.createMember(request)
|
||||||
|
|
||||||
|
response.id shouldBe member.id
|
||||||
|
}
|
||||||
|
|
||||||
|
test("중복된 이메일이 있으면 예외 응답") {
|
||||||
|
every {
|
||||||
|
memberWriter.create(request.name, request.email, request.password, Role.MEMBER)
|
||||||
|
} throws MemberException(MemberErrorCode.DUPLICATE_EMAIL)
|
||||||
|
|
||||||
|
shouldThrow<MemberException> {
|
||||||
|
memberService.createMember(request)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe MemberErrorCode.DUPLICATE_EMAIL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -1,57 +1,53 @@
|
|||||||
package roomescape.member.controller
|
package roomescape.member.controller
|
||||||
|
|
||||||
|
import com.ninjasquad.springmockk.MockkBean
|
||||||
import io.kotest.assertions.assertSoftly
|
import io.kotest.assertions.assertSoftly
|
||||||
import io.kotest.matchers.collections.shouldContainAll
|
import io.kotest.matchers.collections.shouldContainAll
|
||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import roomescape.auth.exception.AuthErrorCode
|
import roomescape.auth.exception.AuthErrorCode
|
||||||
|
import roomescape.member.business.MemberService
|
||||||
import roomescape.member.exception.MemberErrorCode
|
import roomescape.member.exception.MemberErrorCode
|
||||||
|
import roomescape.member.exception.MemberException
|
||||||
import roomescape.member.infrastructure.persistence.Role
|
import roomescape.member.infrastructure.persistence.Role
|
||||||
import roomescape.member.web.MemberController
|
import roomescape.member.web.*
|
||||||
import roomescape.member.web.MemberRetrieveListResponse
|
|
||||||
import roomescape.member.web.SignupRequest
|
|
||||||
import roomescape.util.MemberFixture
|
import roomescape.util.MemberFixture
|
||||||
import roomescape.util.RoomescapeApiTest
|
import roomescape.util.RoomescapeApiTest
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@WebMvcTest(controllers = [MemberController::class])
|
@WebMvcTest(controllers = [MemberController::class])
|
||||||
class MemberControllerTest(
|
class MemberControllerTest(val mockMvc: MockMvc) : RoomescapeApiTest() {
|
||||||
@Autowired private val mockMvc: MockMvc
|
@MockkBean
|
||||||
) : RoomescapeApiTest() {
|
private lateinit var memberService: MemberService
|
||||||
|
|
||||||
init {
|
init {
|
||||||
given("GET /members 요청을") {
|
given("GET /members 요청을") {
|
||||||
val endpoint = "/members"
|
val endpoint = "/members"
|
||||||
|
val response = listOf(
|
||||||
every { memberRepository.findAll() } returns listOf(
|
|
||||||
MemberFixture.create(id = Random.nextLong(), name = "name1"),
|
MemberFixture.create(id = Random.nextLong(), name = "name1"),
|
||||||
MemberFixture.create(id = Random.nextLong(), name = "name2"),
|
MemberFixture.create(id = Random.nextLong(), name = "name2"),
|
||||||
MemberFixture.create(id = Random.nextLong(), name = "name3"),
|
MemberFixture.create(id = Random.nextLong(), name = "name3"),
|
||||||
)
|
).toRetrieveListResponse()
|
||||||
|
|
||||||
|
every { memberService.findMembers() } returns response
|
||||||
|
|
||||||
`when`("관리자가 보내면") {
|
`when`("관리자가 보내면") {
|
||||||
loginAsAdmin()
|
loginAsAdmin()
|
||||||
|
|
||||||
then("성공한다.") {
|
then("성공한다.") {
|
||||||
val result: String = runGetTest(
|
val result: MemberRetrieveListResponse = runGetTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
endpoint = endpoint,
|
endpoint = endpoint,
|
||||||
) {
|
) {
|
||||||
status { isOk() }
|
status { isOk() }
|
||||||
}.andReturn().response.contentAsString
|
}.andReturn().readValue(MemberRetrieveListResponse::class.java)
|
||||||
|
|
||||||
val response: MemberRetrieveListResponse = readValue(
|
|
||||||
responseJson = result,
|
|
||||||
valueType = MemberRetrieveListResponse::class.java
|
|
||||||
)
|
|
||||||
|
|
||||||
assertSoftly(response.members) {
|
assertSoftly(result.members) {
|
||||||
it.size shouldBe 3
|
it.size shouldBe response.members.size
|
||||||
it.map { m -> m.name } shouldContainAll listOf("name1", "name2", "name3")
|
it.map { m -> m.name } shouldContainAll response.members.map { m -> m.name }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,18 +92,14 @@ class MemberControllerTest(
|
|||||||
)
|
)
|
||||||
`when`("같은 이메일이 없으면") {
|
`when`("같은 이메일이 없으면") {
|
||||||
every {
|
every {
|
||||||
memberRepository.findByEmail(request.email)
|
memberService.createMember(request)
|
||||||
} returns null
|
|
||||||
|
|
||||||
every {
|
|
||||||
memberRepository.save(any())
|
|
||||||
} returns MemberFixture.create(
|
} returns MemberFixture.create(
|
||||||
id = 1,
|
id = 1,
|
||||||
name = request.name,
|
name = request.name,
|
||||||
account = request.email,
|
account = request.email,
|
||||||
password = request.password,
|
password = request.password,
|
||||||
role = Role.MEMBER
|
role = Role.MEMBER
|
||||||
)
|
).toSignupResponse()
|
||||||
|
|
||||||
then("id과 이름을 담아 성공 응답") {
|
then("id과 이름을 담아 성공 응답") {
|
||||||
runPostTest(
|
runPostTest(
|
||||||
@ -123,13 +115,12 @@ class MemberControllerTest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
`when`("같은 이메일이 있으면") {
|
`when`("같은 이메일이 있으면") {
|
||||||
|
val expectedError = MemberErrorCode.DUPLICATE_EMAIL
|
||||||
every {
|
every {
|
||||||
memberRepository.findByEmail(request.email)
|
memberService.createMember(request)
|
||||||
} returns mockk()
|
} throws MemberException(expectedError)
|
||||||
|
|
||||||
then("에러 응답") {
|
then("에러 응답") {
|
||||||
val expectedError = MemberErrorCode.DUPLICATE_EMAIL
|
|
||||||
|
|
||||||
runPostTest(
|
runPostTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
endpoint = endpoint,
|
endpoint = endpoint,
|
||||||
@ -138,7 +129,6 @@ class MemberControllerTest(
|
|||||||
status { isEqualTo(expectedError.httpStatus.value()) }
|
status { isEqualTo(expectedError.httpStatus.value()) }
|
||||||
jsonPath("$.code") { value(expectedError.errorCode) }
|
jsonPath("$.code") { value(expectedError.errorCode) }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,85 @@
|
|||||||
|
package roomescape.member.implement
|
||||||
|
|
||||||
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.collections.shouldHaveSize
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
|
import roomescape.member.exception.MemberErrorCode
|
||||||
|
import roomescape.member.exception.MemberException
|
||||||
|
import roomescape.member.infrastructure.persistence.MemberRepository
|
||||||
|
|
||||||
|
class MemberFinderTest : FunSpec({
|
||||||
|
|
||||||
|
val memberRepository: MemberRepository = mockk()
|
||||||
|
val memberFinder = MemberFinder(memberRepository)
|
||||||
|
|
||||||
|
context("findAll") {
|
||||||
|
test("모든 회원을 조회한다.") {
|
||||||
|
every {
|
||||||
|
memberRepository.findAll()
|
||||||
|
} returns listOf(mockk(), mockk(), mockk())
|
||||||
|
|
||||||
|
memberFinder.findAll() shouldHaveSize 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("findById") {
|
||||||
|
val memberId = 1L
|
||||||
|
test("동일한 ID인 회원을 찾아 응답한다.") {
|
||||||
|
every {
|
||||||
|
memberRepository.findByIdOrNull(memberId)
|
||||||
|
} returns mockk()
|
||||||
|
|
||||||
|
memberFinder.findById(memberId)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
memberRepository.findByIdOrNull(memberId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("동일한 ID인 회원이 없으면 실패한다.") {
|
||||||
|
every {
|
||||||
|
memberRepository.findByIdOrNull(memberId)
|
||||||
|
} returns null
|
||||||
|
|
||||||
|
shouldThrow<MemberException> {
|
||||||
|
memberFinder.findById(memberId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe MemberErrorCode.MEMBER_NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("findByEmailAndPassword") {
|
||||||
|
val email = "email"
|
||||||
|
val password = "password"
|
||||||
|
|
||||||
|
test("동일한 이메일과 비밀번호를 가진 회원을 찾아 응답한다.") {
|
||||||
|
every {
|
||||||
|
memberRepository.findByEmailAndPassword(email, password)
|
||||||
|
} returns mockk()
|
||||||
|
|
||||||
|
memberFinder.findByEmailAndPassword(email, password)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
memberRepository.findByEmailAndPassword(email, password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("동일한 이메일과 비밀번호를 가진 회원이 없으면 실패한다.") {
|
||||||
|
every {
|
||||||
|
memberRepository.findByEmailAndPassword(email, password)
|
||||||
|
} returns null
|
||||||
|
|
||||||
|
shouldThrow<MemberException> {
|
||||||
|
memberFinder.findByEmailAndPassword(email, password)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe MemberErrorCode.MEMBER_NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
package roomescape.member.implement
|
||||||
|
|
||||||
|
import io.kotest.assertions.throwables.shouldNotThrow
|
||||||
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.core.spec.style.StringSpec
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import roomescape.member.exception.MemberErrorCode
|
||||||
|
import roomescape.member.exception.MemberException
|
||||||
|
import roomescape.member.infrastructure.persistence.MemberRepository
|
||||||
|
|
||||||
|
class MemberValidatorTest : FunSpec({
|
||||||
|
|
||||||
|
val memberRepository: MemberRepository = mockk()
|
||||||
|
val memberValidator = MemberValidator(memberRepository)
|
||||||
|
|
||||||
|
context("validateCanSignup") {
|
||||||
|
val email = "email@email.com"
|
||||||
|
|
||||||
|
test("같은 이메일을 가진 회원이 있으면 예외를 던진다.") {
|
||||||
|
every {
|
||||||
|
memberRepository.existsByEmail(email)
|
||||||
|
} returns true
|
||||||
|
|
||||||
|
shouldThrow<MemberException> {
|
||||||
|
memberValidator.validateCanSignup(email)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe MemberErrorCode.DUPLICATE_EMAIL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("같은 이메일을 가진 회원이 없으면 종료한다.") {
|
||||||
|
every {
|
||||||
|
memberRepository.existsByEmail(email)
|
||||||
|
} returns false
|
||||||
|
|
||||||
|
shouldNotThrow<MemberException> {
|
||||||
|
memberValidator.validateCanSignup(email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
package roomescape.member.implement
|
||||||
|
|
||||||
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.core.spec.style.StringSpec
|
||||||
|
import io.kotest.matchers.nulls.shouldNotBeNull
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.slot
|
||||||
|
import io.mockk.spyk
|
||||||
|
import io.mockk.verify
|
||||||
|
import roomescape.member.exception.MemberErrorCode
|
||||||
|
import roomescape.member.exception.MemberException
|
||||||
|
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||||
|
import roomescape.member.infrastructure.persistence.MemberRepository
|
||||||
|
import roomescape.member.infrastructure.persistence.Role
|
||||||
|
import roomescape.util.MemberFixture
|
||||||
|
import roomescape.util.TsidFactory
|
||||||
|
|
||||||
|
class MemberWriterTest : FunSpec({
|
||||||
|
val memberRepository: MemberRepository = mockk()
|
||||||
|
val memberValidator = MemberValidator(memberRepository)
|
||||||
|
val memberWriter = MemberWriter(TsidFactory, memberValidator, memberRepository)
|
||||||
|
|
||||||
|
context("create") {
|
||||||
|
val name = "name"
|
||||||
|
val email = "email"
|
||||||
|
val password = "password"
|
||||||
|
val role = Role.MEMBER
|
||||||
|
|
||||||
|
test("중복된 이메일이 있으면 실패한다.") {
|
||||||
|
every {
|
||||||
|
memberRepository.existsByEmail(any())
|
||||||
|
} returns true
|
||||||
|
|
||||||
|
shouldThrow<MemberException> {
|
||||||
|
memberWriter.create(name, email, password, role)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe MemberErrorCode.DUPLICATE_EMAIL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("중복된 이메일이 없으면 저장한다.") {
|
||||||
|
every {
|
||||||
|
memberRepository.existsByEmail(any())
|
||||||
|
} returns false
|
||||||
|
|
||||||
|
every {
|
||||||
|
memberRepository.save(any())
|
||||||
|
} returns MemberFixture.create(
|
||||||
|
name = name,
|
||||||
|
account = email,
|
||||||
|
password = password,
|
||||||
|
role = role
|
||||||
|
)
|
||||||
|
|
||||||
|
memberWriter.create(name, email, password, role)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
memberRepository.save(any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
package roomescape.member.infrastructure.persistence
|
||||||
|
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.nulls.shouldNotBeNull
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import jakarta.persistence.EntityManager
|
||||||
|
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||||
|
import roomescape.util.MemberFixture
|
||||||
|
|
||||||
|
@DataJpaTest(showSql = true)
|
||||||
|
class MemberRepositoryTest(
|
||||||
|
val entityManager: EntityManager,
|
||||||
|
val memberRepository: MemberRepository
|
||||||
|
) : FunSpec({
|
||||||
|
context("existsByEmail") {
|
||||||
|
val account = "email"
|
||||||
|
val email = "$account@email.com"
|
||||||
|
|
||||||
|
beforeTest {
|
||||||
|
entityManager.persist(MemberFixture.create(account = account))
|
||||||
|
entityManager.flush()
|
||||||
|
entityManager.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
test("동일한 이메일이 있으면 true 반환") {
|
||||||
|
val result = memberRepository.existsByEmail(email)
|
||||||
|
result shouldBe true
|
||||||
|
}
|
||||||
|
|
||||||
|
test("동일한 이메일이 없으면 false 반환") {
|
||||||
|
memberRepository.existsByEmail(email.substring(email.length - 1)) shouldBe false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("findByEmailAndPassword") {
|
||||||
|
val account = "email"
|
||||||
|
val email = "$account@email.com"
|
||||||
|
val password = "password123"
|
||||||
|
|
||||||
|
beforeTest {
|
||||||
|
entityManager.persist(MemberFixture.create(account = account, password = password))
|
||||||
|
entityManager.flush()
|
||||||
|
entityManager.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
test("둘다 일치하면 정상 반환") {
|
||||||
|
memberRepository.findByEmailAndPassword(email, password) shouldNotBeNull {
|
||||||
|
this.email shouldBe email
|
||||||
|
this.password shouldBe password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("이메일이 틀리면 null 반환") {
|
||||||
|
val invalidMail = email.substring(email.length - 1)
|
||||||
|
memberRepository.findByEmailAndPassword(invalidMail, password) shouldBe null
|
||||||
|
}
|
||||||
|
|
||||||
|
test("비밀번호가 틀리면 null 반환") {
|
||||||
|
val invalidPassword = password.substring(password.length - 1)
|
||||||
|
memberRepository.findByEmailAndPassword(email, invalidPassword) shouldBe null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -4,114 +4,162 @@ import io.kotest.assertions.assertSoftly
|
|||||||
import io.kotest.assertions.throwables.shouldThrow
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.kotest.matchers.types.shouldBeInstanceOf
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.runs
|
import io.mockk.slot
|
||||||
import roomescape.payment.exception.PaymentErrorCode
|
import roomescape.payment.exception.PaymentErrorCode
|
||||||
import roomescape.payment.exception.PaymentException
|
import roomescape.payment.exception.PaymentException
|
||||||
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository
|
import roomescape.payment.implement.PaymentFinder
|
||||||
import roomescape.payment.infrastructure.persistence.PaymentRepository
|
import roomescape.payment.implement.PaymentWriter
|
||||||
|
import roomescape.payment.infrastructure.client.PaymentApproveResponse
|
||||||
|
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity
|
||||||
import roomescape.payment.web.PaymentCancelRequest
|
import roomescape.payment.web.PaymentCancelRequest
|
||||||
|
import roomescape.payment.web.PaymentCancelResponse
|
||||||
import roomescape.util.PaymentFixture
|
import roomescape.util.PaymentFixture
|
||||||
import roomescape.util.TsidFactory
|
import roomescape.util.ReservationFixture
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalTime
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
class PaymentServiceTest : FunSpec({
|
class PaymentServiceTest : FunSpec({
|
||||||
val paymentRepository: PaymentRepository = mockk()
|
val paymentFinder: PaymentFinder = mockk()
|
||||||
val canceledPaymentRepository: CanceledPaymentRepository = mockk()
|
val paymentWriter: PaymentWriter = mockk()
|
||||||
|
|
||||||
val paymentService = PaymentService(TsidFactory, paymentRepository, canceledPaymentRepository)
|
val paymentService = PaymentService(paymentFinder, paymentWriter)
|
||||||
|
|
||||||
context("createCanceledPaymentByReservationId") {
|
context("createPayment") {
|
||||||
val reservationId = 1L
|
val approvedPaymentInfo = PaymentApproveResponse(
|
||||||
test("reservationId로 paymentKey를 찾을 수 없으면 예외를 던진다.") {
|
paymentKey = "paymentKey",
|
||||||
every { paymentRepository.findPaymentKeyByReservationId(reservationId) } returns null
|
orderId = "orderId",
|
||||||
|
totalAmount = 1000L,
|
||||||
|
approvedAt = OffsetDateTime.now(),
|
||||||
|
)
|
||||||
|
val reservation = ReservationFixture.create(id = 1L)
|
||||||
|
|
||||||
val exception = shouldThrow<PaymentException> {
|
test("정상 응답") {
|
||||||
paymentService.createCanceledPaymentByReservationId(reservationId)
|
every {
|
||||||
|
paymentWriter.create(
|
||||||
|
paymentKey = approvedPaymentInfo.paymentKey,
|
||||||
|
orderId = approvedPaymentInfo.orderId,
|
||||||
|
totalAmount = approvedPaymentInfo.totalAmount,
|
||||||
|
approvedAt = approvedPaymentInfo.approvedAt,
|
||||||
|
reservation = reservation
|
||||||
|
)
|
||||||
|
} returns PaymentFixture.create(
|
||||||
|
id = 1L,
|
||||||
|
orderId = approvedPaymentInfo.orderId,
|
||||||
|
paymentKey = approvedPaymentInfo.paymentKey,
|
||||||
|
totalAmount = approvedPaymentInfo.totalAmount,
|
||||||
|
approvedAt = approvedPaymentInfo.approvedAt,
|
||||||
|
reservation = reservation
|
||||||
|
)
|
||||||
|
|
||||||
|
val response = paymentService.createPayment(approvedPaymentInfo, reservation)
|
||||||
|
|
||||||
|
assertSoftly(response) {
|
||||||
|
it.id shouldBe 1L
|
||||||
|
it.paymentKey shouldBe approvedPaymentInfo.paymentKey
|
||||||
|
it.reservationId shouldBe reservation.id
|
||||||
}
|
}
|
||||||
exception.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
context("reservationId로 paymentKey를 찾고난 후") {
|
context("createCanceledPayment(canceledPaymentInfo)") {
|
||||||
val paymentKey = "test-payment-key"
|
val canceledPaymentInfo = PaymentCancelResponse(
|
||||||
|
cancelStatus = "normal",
|
||||||
|
cancelReason = "고객 요청",
|
||||||
|
cancelAmount = 1000L,
|
||||||
|
canceledAt = OffsetDateTime.now(),
|
||||||
|
)
|
||||||
|
val approvedAt = OffsetDateTime.now()
|
||||||
|
val paymentKey = "paymentKey"
|
||||||
|
|
||||||
|
test("CanceledPaymentEntity를 응답") {
|
||||||
|
every {
|
||||||
|
paymentWriter.createCanceled(
|
||||||
|
cancelReason = canceledPaymentInfo.cancelReason,
|
||||||
|
cancelAmount = canceledPaymentInfo.cancelAmount,
|
||||||
|
canceledAt = canceledPaymentInfo.canceledAt,
|
||||||
|
approvedAt = approvedAt,
|
||||||
|
paymentKey = paymentKey
|
||||||
|
)
|
||||||
|
} returns PaymentFixture.createCanceled(
|
||||||
|
id = 1L,
|
||||||
|
paymentKey = paymentKey,
|
||||||
|
cancelAmount = canceledPaymentInfo.cancelAmount
|
||||||
|
)
|
||||||
|
|
||||||
|
val response = paymentService.createCanceledPayment(canceledPaymentInfo, approvedAt, paymentKey)
|
||||||
|
|
||||||
|
response.shouldBeInstanceOf<CanceledPaymentEntity>()
|
||||||
|
response.paymentKey shouldBe paymentKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("createCanceledPayment(reservationId)") {
|
||||||
|
val reservationId = 1L
|
||||||
|
|
||||||
|
test("취소 사유를 '예약 취소'로 하여 PaymentCancelRequest를 응답") {
|
||||||
|
val payment = PaymentFixture.create(id = 1L, paymentKey = "paymentKey", totalAmount = 1000L)
|
||||||
|
every {
|
||||||
|
paymentFinder.findByReservationId(reservationId)
|
||||||
|
} returns payment
|
||||||
|
|
||||||
|
val cancelReasonSlot = slot<String>()
|
||||||
|
|
||||||
every {
|
every {
|
||||||
paymentRepository.findPaymentKeyByReservationId(reservationId)
|
paymentWriter.createCanceled(payment, capture(cancelReasonSlot), any())
|
||||||
} returns paymentKey
|
} returns PaymentFixture.createCanceled(
|
||||||
|
id = 1L,
|
||||||
|
paymentKey = payment.paymentKey,
|
||||||
|
cancelAmount = payment.totalAmount
|
||||||
|
)
|
||||||
|
|
||||||
test("해당 paymentKey로 paymentEntity를 찾을 수 없으면 예외를 던진다.") {
|
val response = paymentService.createCanceledPayment(reservationId)
|
||||||
every {
|
|
||||||
paymentRepository.findByPaymentKey(paymentKey)
|
|
||||||
} returns null
|
|
||||||
|
|
||||||
val exception = shouldThrow<PaymentException> {
|
response.shouldBeInstanceOf<PaymentCancelRequest>()
|
||||||
paymentService.createCanceledPaymentByReservationId(reservationId)
|
cancelReasonSlot.captured shouldBe "예약 취소"
|
||||||
}
|
}
|
||||||
exception.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND
|
|
||||||
}
|
|
||||||
|
|
||||||
test("해당 paymentKey로 paymentEntity를 찾고, cancelPaymentEntity를 저장한다.") {
|
test("결제 정보가 없으면 예외 응답") {
|
||||||
val paymentEntity = PaymentFixture.create(paymentKey = paymentKey)
|
every {
|
||||||
|
paymentFinder.findByReservationId(reservationId)
|
||||||
|
} throws PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND)
|
||||||
|
|
||||||
every {
|
shouldThrow<PaymentException> {
|
||||||
paymentRepository.findByPaymentKey(paymentKey)
|
paymentService.createCanceledPayment(reservationId)
|
||||||
} returns paymentEntity.also {
|
}.also {
|
||||||
every {
|
it.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND
|
||||||
paymentRepository.delete(it)
|
|
||||||
} just runs
|
|
||||||
}
|
|
||||||
|
|
||||||
every {
|
|
||||||
canceledPaymentRepository.save(any())
|
|
||||||
} returns PaymentFixture.createCanceled(
|
|
||||||
id = 1L,
|
|
||||||
paymentKey = paymentKey,
|
|
||||||
cancelReason = "Test",
|
|
||||||
cancelAmount = paymentEntity.totalAmount,
|
|
||||||
)
|
|
||||||
|
|
||||||
val result: PaymentCancelRequest = paymentService.createCanceledPaymentByReservationId(reservationId)
|
|
||||||
|
|
||||||
assertSoftly(result) {
|
|
||||||
this.paymentKey shouldBe paymentKey
|
|
||||||
this.amount shouldBe paymentEntity.totalAmount
|
|
||||||
this.cancelReason shouldBe "Test"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context("updateCanceledTime") {
|
context("updateCanceledTime") {
|
||||||
val paymentKey = "test-payment-key"
|
val paymentKey = "paymentKey"
|
||||||
val canceledAt = OffsetDateTime.now()
|
val canceledAt = OffsetDateTime.of(LocalDate.of(2025, 8, 5), LocalTime.of(10, 0), ZoneOffset.UTC)
|
||||||
|
|
||||||
test("paymentKey로 canceledPaymentEntity를 찾을 수 없으면 예외를 던진다.") {
|
test("정상 응답") {
|
||||||
|
val canceled = PaymentFixture.createCanceled(id = 1L)
|
||||||
every {
|
every {
|
||||||
canceledPaymentRepository.findByPaymentKey(paymentKey)
|
paymentFinder.findCanceledByKey(paymentKey)
|
||||||
} returns null
|
} returns canceled
|
||||||
|
|
||||||
val exception = shouldThrow<PaymentException> {
|
|
||||||
paymentService.updateCanceledTime(paymentKey, canceledAt)
|
|
||||||
}
|
|
||||||
exception.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND
|
|
||||||
}
|
|
||||||
|
|
||||||
test("paymentKey로 canceledPaymentEntity를 찾고, canceledAt을 업데이트한다.") {
|
|
||||||
val canceledPaymentEntity = PaymentFixture.createCanceled(
|
|
||||||
paymentKey = paymentKey,
|
|
||||||
canceledAt = canceledAt.minusMinutes(1)
|
|
||||||
)
|
|
||||||
|
|
||||||
every {
|
|
||||||
canceledPaymentRepository.findByPaymentKey(paymentKey)
|
|
||||||
} returns canceledPaymentEntity
|
|
||||||
|
|
||||||
paymentService.updateCanceledTime(paymentKey, canceledAt)
|
paymentService.updateCanceledTime(paymentKey, canceledAt)
|
||||||
|
|
||||||
assertSoftly(canceledPaymentEntity) {
|
canceled.canceledAt shouldBe canceledAt
|
||||||
this.canceledAt shouldBe canceledAt
|
}
|
||||||
|
|
||||||
|
test("결제 취소 정보가 없으면 예외 응답") {
|
||||||
|
every {
|
||||||
|
paymentFinder.findCanceledByKey(paymentKey)
|
||||||
|
} throws PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND)
|
||||||
|
|
||||||
|
shouldThrow<PaymentException> {
|
||||||
|
paymentService.updateCanceledTime(paymentKey, canceledAt)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,93 @@
|
|||||||
|
package roomescape.payment.implement
|
||||||
|
|
||||||
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import roomescape.payment.exception.PaymentErrorCode
|
||||||
|
import roomescape.payment.exception.PaymentException
|
||||||
|
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository
|
||||||
|
import roomescape.payment.infrastructure.persistence.PaymentRepository
|
||||||
|
|
||||||
|
class PaymentFinderTest : FunSpec({
|
||||||
|
val paymentRepository: PaymentRepository = mockk()
|
||||||
|
val canceledPaymentRepository: CanceledPaymentRepository = mockk()
|
||||||
|
|
||||||
|
val paymentFinder = PaymentFinder(paymentRepository, canceledPaymentRepository)
|
||||||
|
|
||||||
|
context("existsPaymentByReservationId") {
|
||||||
|
val reservationId = 1L
|
||||||
|
test("결제 정보가 있으면 true를 반환한다.") {
|
||||||
|
every {
|
||||||
|
paymentRepository.existsByReservationId(reservationId)
|
||||||
|
} returns true
|
||||||
|
|
||||||
|
paymentFinder.existsPaymentByReservationId(reservationId) shouldBe true
|
||||||
|
}
|
||||||
|
|
||||||
|
test("결제 정보가 없으면 false를 반환한다.") {
|
||||||
|
every {
|
||||||
|
paymentRepository.existsByReservationId(reservationId)
|
||||||
|
} returns false
|
||||||
|
|
||||||
|
paymentFinder.existsPaymentByReservationId(reservationId) shouldBe false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("findByReservationId") {
|
||||||
|
val reservationId = 1L
|
||||||
|
test("결제 정보를 조회한다.") {
|
||||||
|
every {
|
||||||
|
paymentRepository.findByReservationId(reservationId)
|
||||||
|
} returns mockk()
|
||||||
|
|
||||||
|
paymentFinder.findByReservationId(reservationId)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
paymentRepository.findByReservationId(reservationId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("결제 정보가 없으면 실패한다.") {
|
||||||
|
every {
|
||||||
|
paymentRepository.findByReservationId(reservationId)
|
||||||
|
} returns null
|
||||||
|
|
||||||
|
shouldThrow<PaymentException> {
|
||||||
|
paymentFinder.findByReservationId(reservationId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("findCanceledByKey") {
|
||||||
|
val paymentKey = "paymentKey"
|
||||||
|
|
||||||
|
test("결제 취소 정보를 조회한다.") {
|
||||||
|
every {
|
||||||
|
canceledPaymentRepository.findByPaymentKey(paymentKey)
|
||||||
|
} returns mockk()
|
||||||
|
|
||||||
|
paymentFinder.findCanceledByKey(paymentKey)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
canceledPaymentRepository.findByPaymentKey(paymentKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("결제 취소 정보가 없으면 실패한다.") {
|
||||||
|
every {
|
||||||
|
canceledPaymentRepository.findByPaymentKey(paymentKey)
|
||||||
|
} returns null
|
||||||
|
|
||||||
|
shouldThrow<PaymentException> {
|
||||||
|
paymentFinder.findCanceledByKey(paymentKey)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe PaymentErrorCode.CANCELED_PAYMENT_NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -0,0 +1,121 @@
|
|||||||
|
package roomescape.payment.implement
|
||||||
|
|
||||||
|
import com.ninjasquad.springmockk.MockkClear
|
||||||
|
import com.ninjasquad.springmockk.clear
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.date.after
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.clearMocks
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.slot
|
||||||
|
import io.mockk.verify
|
||||||
|
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity
|
||||||
|
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository
|
||||||
|
import roomescape.payment.infrastructure.persistence.PaymentEntity
|
||||||
|
import roomescape.payment.infrastructure.persistence.PaymentRepository
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||||
|
import roomescape.util.PaymentFixture
|
||||||
|
import roomescape.util.ReservationFixture
|
||||||
|
import roomescape.util.TsidFactory
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
class PaymentWriterTest : FunSpec({
|
||||||
|
val paymentRepository: PaymentRepository = mockk()
|
||||||
|
val canceledPaymentRepository: CanceledPaymentRepository = mockk()
|
||||||
|
|
||||||
|
val paymentWriter = PaymentWriter(paymentRepository, canceledPaymentRepository, TsidFactory)
|
||||||
|
|
||||||
|
val paymentKey = "paymentKey"
|
||||||
|
val orderId = "orderId"
|
||||||
|
val totalAmount = 1000L
|
||||||
|
val approvedAt = OffsetDateTime.now()
|
||||||
|
|
||||||
|
context("create") {
|
||||||
|
val reservation: ReservationEntity = ReservationFixture.create(id = 1L)
|
||||||
|
test("결제 정보를 저장한다.") {
|
||||||
|
val slot = slot<PaymentEntity>()
|
||||||
|
every {
|
||||||
|
paymentRepository.save(capture(slot))
|
||||||
|
} returns mockk()
|
||||||
|
|
||||||
|
paymentWriter.create(paymentKey, orderId, totalAmount, approvedAt, reservation)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
paymentRepository.save(any())
|
||||||
|
}
|
||||||
|
|
||||||
|
slot.captured.also {
|
||||||
|
it.paymentKey shouldBe paymentKey
|
||||||
|
it.orderId shouldBe orderId
|
||||||
|
it.totalAmount shouldBe totalAmount
|
||||||
|
it.approvedAt shouldBe approvedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
paymentRepository.clear(MockkClear.AFTER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("createCanceled") {
|
||||||
|
val cancelReason = "고객 요청"
|
||||||
|
val canceledAt = OffsetDateTime.now()
|
||||||
|
|
||||||
|
afterTest {
|
||||||
|
clearMocks(paymentRepository)
|
||||||
|
clearMocks(canceledPaymentRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("PaymentEntity를 받아 저장한다.") {
|
||||||
|
val payment: PaymentEntity = PaymentFixture.create(id = 1L)
|
||||||
|
val slot = slot<CanceledPaymentEntity>()
|
||||||
|
|
||||||
|
every {
|
||||||
|
canceledPaymentRepository.save(capture(slot))
|
||||||
|
} returns mockk()
|
||||||
|
|
||||||
|
every {
|
||||||
|
paymentRepository.deleteByPaymentKey(paymentKey)
|
||||||
|
} returns Unit
|
||||||
|
|
||||||
|
paymentWriter.createCanceled(payment, cancelReason, canceledAt)
|
||||||
|
|
||||||
|
verify(exactly = 1) { canceledPaymentRepository.save(any()) }
|
||||||
|
verify(exactly = 1) { paymentRepository.deleteByPaymentKey(any()) }
|
||||||
|
|
||||||
|
slot.captured.also {
|
||||||
|
it.paymentKey shouldBe payment.paymentKey
|
||||||
|
it.cancelAmount shouldBe payment.totalAmount
|
||||||
|
it.approvedAt shouldBe payment.approvedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("취소 정보를 받아 저장한다.") {
|
||||||
|
val slot = slot<CanceledPaymentEntity>()
|
||||||
|
|
||||||
|
every {
|
||||||
|
canceledPaymentRepository.save(capture(slot))
|
||||||
|
} returns mockk()
|
||||||
|
|
||||||
|
every {
|
||||||
|
paymentRepository.deleteByPaymentKey(paymentKey)
|
||||||
|
} returns Unit
|
||||||
|
|
||||||
|
paymentWriter.createCanceled(
|
||||||
|
cancelReason = cancelReason,
|
||||||
|
cancelAmount = totalAmount,
|
||||||
|
canceledAt = canceledAt,
|
||||||
|
approvedAt = approvedAt,
|
||||||
|
paymentKey = paymentKey
|
||||||
|
)
|
||||||
|
|
||||||
|
verify(exactly = 1) { canceledPaymentRepository.save(any()) }
|
||||||
|
verify(exactly = 1) { paymentRepository.deleteByPaymentKey(any()) }
|
||||||
|
|
||||||
|
slot.captured.also {
|
||||||
|
it.paymentKey shouldBe paymentKey
|
||||||
|
it.cancelAmount shouldBe totalAmount
|
||||||
|
it.approvedAt shouldBe approvedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -48,7 +48,6 @@ class TossPaymentClientTest(
|
|||||||
.createResponse(it)
|
.createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// when
|
|
||||||
val paymentRequest = SampleTossPaymentConst.paymentRequest
|
val paymentRequest = SampleTossPaymentConst.paymentRequest
|
||||||
val paymentResponse: PaymentApproveResponse = client.confirm(paymentRequest)
|
val paymentResponse: PaymentApproveResponse = client.confirm(paymentRequest)
|
||||||
|
|
||||||
@ -68,7 +67,6 @@ class TossPaymentClientTest(
|
|||||||
.createResponse(it)
|
.createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// when
|
|
||||||
val paymentRequest = SampleTossPaymentConst.paymentRequest
|
val paymentRequest = SampleTossPaymentConst.paymentRequest
|
||||||
|
|
||||||
// then
|
// then
|
||||||
@ -107,7 +105,7 @@ class TossPaymentClientTest(
|
|||||||
.createResponse(it)
|
.createResponse(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// when
|
|
||||||
val cancelRequest: PaymentCancelRequest = SampleTossPaymentConst.cancelRequest
|
val cancelRequest: PaymentCancelRequest = SampleTossPaymentConst.cancelRequest
|
||||||
val cancelResponse: PaymentCancelResponse = client.cancel(cancelRequest)
|
val cancelResponse: PaymentCancelResponse = client.cancel(cancelRequest)
|
||||||
|
|
||||||
|
|||||||
@ -38,58 +38,6 @@ class PaymentRepositoryTest(
|
|||||||
.also { it shouldBe false }
|
.also { it shouldBe false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context("findPaymentKeyByReservationId") {
|
|
||||||
lateinit var paymentKey: String
|
|
||||||
|
|
||||||
beforeTest {
|
|
||||||
reservation = setupReservation()
|
|
||||||
paymentKey = PaymentFixture.create(reservation = reservation)
|
|
||||||
.also { paymentRepository.save(it) }
|
|
||||||
.paymentKey
|
|
||||||
}
|
|
||||||
|
|
||||||
test("정상 반환") {
|
|
||||||
paymentRepository.findPaymentKeyByReservationId(reservation.id!!)
|
|
||||||
?.let { it shouldBe paymentKey }
|
|
||||||
?: throw AssertionError("Unexpected null value")
|
|
||||||
}
|
|
||||||
|
|
||||||
test("null 반환") {
|
|
||||||
paymentRepository.findPaymentKeyByReservationId(reservation.id!! + 1)
|
|
||||||
.also { it shouldBe null }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context("findByPaymentKey") {
|
|
||||||
lateinit var payment: PaymentEntity
|
|
||||||
|
|
||||||
beforeTest {
|
|
||||||
reservation = setupReservation()
|
|
||||||
payment = PaymentFixture.create(reservation = reservation)
|
|
||||||
.also { paymentRepository.save(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
test("정상 반환") {
|
|
||||||
paymentRepository.findByPaymentKey(payment.paymentKey)
|
|
||||||
?.also {
|
|
||||||
assertSoftly(it) {
|
|
||||||
this.id shouldBe payment.id
|
|
||||||
this.orderId shouldBe payment.orderId
|
|
||||||
this.paymentKey shouldBe payment.paymentKey
|
|
||||||
this.totalAmount shouldBe payment.totalAmount
|
|
||||||
this.reservation.id shouldBe payment.reservation.id
|
|
||||||
this.approvedAt shouldBe payment.approvedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?: throw AssertionError("Unexpected null value")
|
|
||||||
}
|
|
||||||
|
|
||||||
test("null 반환") {
|
|
||||||
paymentRepository.findByPaymentKey("non-existent-key")
|
|
||||||
.also { it shouldBe null }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupReservation(): ReservationEntity {
|
private fun setupReservation(): ReservationEntity {
|
||||||
|
|||||||
@ -0,0 +1,284 @@
|
|||||||
|
package roomescape.reservation.business
|
||||||
|
|
||||||
|
import io.kotest.assertions.assertSoftly
|
||||||
|
import io.kotest.assertions.throwables.shouldNotThrow
|
||||||
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.*
|
||||||
|
import roomescape.reservation.exception.ReservationErrorCode
|
||||||
|
import roomescape.reservation.exception.ReservationException
|
||||||
|
import roomescape.reservation.implement.ReservationFinder
|
||||||
|
import roomescape.reservation.implement.ReservationWriter
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||||
|
import roomescape.util.MemberFixture
|
||||||
|
import roomescape.util.ReservationFixture
|
||||||
|
import roomescape.util.ThemeFixture
|
||||||
|
import roomescape.util.TimeFixture
|
||||||
|
|
||||||
|
class ReservationCommandServiceTest : FunSpec({
|
||||||
|
|
||||||
|
val reservationFinder: ReservationFinder = mockk()
|
||||||
|
val reservationWriter: ReservationWriter = mockk()
|
||||||
|
val reservationWriteService = ReservationWriteService(reservationFinder, reservationWriter)
|
||||||
|
|
||||||
|
context("createReservationWithPayment") {
|
||||||
|
val request = ReservationFixture.createRequest()
|
||||||
|
val memberId = 1L
|
||||||
|
|
||||||
|
test("정상 응답") {
|
||||||
|
val createdReservation = ReservationFixture.create(
|
||||||
|
date = request.date,
|
||||||
|
time = TimeFixture.create(id = request.timeId),
|
||||||
|
theme = ThemeFixture.create(id = request.themeId),
|
||||||
|
member = MemberFixture.create(id = memberId),
|
||||||
|
status = ReservationStatus.CONFIRMED
|
||||||
|
)
|
||||||
|
|
||||||
|
every {
|
||||||
|
reservationWriter.create(
|
||||||
|
date = request.date,
|
||||||
|
timeId = request.timeId,
|
||||||
|
themeId = request.themeId,
|
||||||
|
status = ReservationStatus.CONFIRMED,
|
||||||
|
memberId = memberId,
|
||||||
|
requesterId = memberId
|
||||||
|
)
|
||||||
|
} returns createdReservation
|
||||||
|
|
||||||
|
val result = reservationWriteService.createReservationWithPayment(request, memberId)
|
||||||
|
|
||||||
|
assertSoftly(result) {
|
||||||
|
this.date shouldBe request.date
|
||||||
|
this.time.id shouldBe request.timeId
|
||||||
|
this.theme.id shouldBe request.themeId
|
||||||
|
this.member.id shouldBe memberId
|
||||||
|
this.status shouldBe ReservationStatus.CONFIRMED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("예약 생성에 실패하면 예외 응답") {
|
||||||
|
every {
|
||||||
|
reservationWriter.create(any(), any(), any(), any(), any(), any())
|
||||||
|
} throws ReservationException(ReservationErrorCode.RESERVATION_DUPLICATED)
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationWriteService.createReservationWithPayment(request, memberId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.RESERVATION_DUPLICATED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("createReservationByAdmin") {
|
||||||
|
val request = ReservationFixture.createAdminRequest()
|
||||||
|
val adminId = request.memberId + 1
|
||||||
|
|
||||||
|
test("정상 응답") {
|
||||||
|
val createdReservation = ReservationFixture.create(
|
||||||
|
date = request.date,
|
||||||
|
time = TimeFixture.create(id = request.timeId),
|
||||||
|
theme = ThemeFixture.create(id = request.themeId),
|
||||||
|
member = MemberFixture.create(id = request.memberId),
|
||||||
|
status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||||
|
)
|
||||||
|
|
||||||
|
every {
|
||||||
|
reservationWriter.create(
|
||||||
|
date = request.date,
|
||||||
|
timeId = request.timeId,
|
||||||
|
themeId = request.themeId,
|
||||||
|
status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED,
|
||||||
|
memberId = request.memberId,
|
||||||
|
requesterId = adminId
|
||||||
|
)
|
||||||
|
} returns createdReservation
|
||||||
|
|
||||||
|
val response = reservationWriteService.createReservationByAdmin(request, adminId)
|
||||||
|
|
||||||
|
assertSoftly(response) {
|
||||||
|
this.date shouldBe request.date
|
||||||
|
this.time.id shouldBe request.timeId
|
||||||
|
this.theme.id shouldBe request.themeId
|
||||||
|
this.member.id shouldBe request.memberId
|
||||||
|
this.status shouldBe ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("createWaiting") {
|
||||||
|
val request = ReservationFixture.createWaitingRequest()
|
||||||
|
val memberId = 1L
|
||||||
|
|
||||||
|
test("정상 응답") {
|
||||||
|
val createdWaiting = ReservationFixture.create(
|
||||||
|
date = request.date,
|
||||||
|
time = TimeFixture.create(id = request.timeId),
|
||||||
|
theme = ThemeFixture.create(id = request.themeId),
|
||||||
|
member = MemberFixture.create(id = memberId),
|
||||||
|
status = ReservationStatus.WAITING
|
||||||
|
)
|
||||||
|
|
||||||
|
every {
|
||||||
|
reservationWriter.create(
|
||||||
|
date = request.date,
|
||||||
|
timeId = request.timeId,
|
||||||
|
themeId = request.themeId,
|
||||||
|
status = ReservationStatus.WAITING,
|
||||||
|
memberId = memberId,
|
||||||
|
requesterId = memberId
|
||||||
|
)
|
||||||
|
} returns createdWaiting
|
||||||
|
|
||||||
|
val response = reservationWriteService.createWaiting(request, memberId)
|
||||||
|
|
||||||
|
assertSoftly(response) {
|
||||||
|
this.date shouldBe request.date
|
||||||
|
this.time.id shouldBe request.timeId
|
||||||
|
this.theme.id shouldBe request.themeId
|
||||||
|
this.member.id shouldBe memberId
|
||||||
|
this.status shouldBe ReservationStatus.WAITING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("이미 예약한 내역이 있으면 예외 응답") {
|
||||||
|
every {
|
||||||
|
reservationWriter.create(any(), any(), any(), any(), any(), any())
|
||||||
|
} throws ReservationException(ReservationErrorCode.ALREADY_RESERVE)
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationWriteService.createWaiting(request, memberId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.ALREADY_RESERVE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("deleteReservation") {
|
||||||
|
val reservationId = 1L
|
||||||
|
val memberId = 1L
|
||||||
|
val reservation = ReservationFixture.create(id = reservationId, member = MemberFixture.create(id = memberId))
|
||||||
|
|
||||||
|
test("정상 응답") {
|
||||||
|
every { reservationFinder.findById(reservationId) } returns reservation
|
||||||
|
every { reservationWriter.deleteConfirmed(reservation, memberId) } just Runs
|
||||||
|
|
||||||
|
shouldNotThrow<Exception> {
|
||||||
|
reservationWriteService.deleteReservation(reservationId, memberId)
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(exactly = 1) { reservationWriter.deleteConfirmed(reservation, memberId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
test("예약을 찾을 수 없으면 예외 응답") {
|
||||||
|
every {
|
||||||
|
reservationFinder.findById(reservationId)
|
||||||
|
} throws ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND)
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationWriteService.deleteReservation(reservationId, memberId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.RESERVATION_NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("삭제하려는 회원이 관리자가 아니고, 대기한 회원과 다르면 예외 응답") {
|
||||||
|
every { reservationFinder.findById(reservationId) } returns reservation
|
||||||
|
every {
|
||||||
|
reservationWriter.deleteConfirmed(reservation, memberId)
|
||||||
|
} throws ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER)
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationWriteService.deleteReservation(reservationId, memberId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.NOT_RESERVATION_OWNER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("confirmWaiting") {
|
||||||
|
val reservationId = 1L
|
||||||
|
val memberId = 99L // Admin
|
||||||
|
|
||||||
|
test("정상 응답") {
|
||||||
|
every { reservationWriter.confirm(reservationId) } just Runs
|
||||||
|
|
||||||
|
shouldNotThrow<Exception> {
|
||||||
|
reservationWriteService.confirmWaiting(reservationId, memberId)
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(exactly = 1) { reservationWriter.confirm(reservationId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
test("이미 확정된 예약이 있으면 예외 응답") {
|
||||||
|
every {
|
||||||
|
reservationWriter.confirm(reservationId)
|
||||||
|
} throws ReservationException(ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS)
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationWriteService.confirmWaiting(reservationId, memberId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("deleteWaiting") {
|
||||||
|
val reservationId = 1L
|
||||||
|
val memberId = 1L
|
||||||
|
val waitingReservation = ReservationFixture.create(
|
||||||
|
id = reservationId,
|
||||||
|
member = MemberFixture.create(id = memberId),
|
||||||
|
status = ReservationStatus.WAITING
|
||||||
|
)
|
||||||
|
|
||||||
|
test("정상 응답") {
|
||||||
|
every { reservationFinder.findById(reservationId) } returns waitingReservation
|
||||||
|
every { reservationWriter.deleteWaiting(waitingReservation, memberId) } just Runs
|
||||||
|
|
||||||
|
shouldNotThrow<Exception> {
|
||||||
|
reservationWriteService.deleteWaiting(reservationId, memberId)
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(exactly = 1) { reservationWriter.deleteWaiting(waitingReservation, memberId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
test("예약을 찾을 수 없으면 예외 응답") {
|
||||||
|
every {
|
||||||
|
reservationFinder.findById(reservationId)
|
||||||
|
} throws ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND)
|
||||||
|
|
||||||
|
shouldThrow< ReservationException> {
|
||||||
|
reservationWriteService.deleteWaiting(reservationId, memberId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.RESERVATION_NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("대기 상태가 아니면 예외 응답") {
|
||||||
|
every { reservationFinder.findById(reservationId) } returns waitingReservation
|
||||||
|
every {
|
||||||
|
reservationWriter.deleteWaiting(waitingReservation, memberId)
|
||||||
|
} throws ReservationException(ReservationErrorCode.ALREADY_CONFIRMED)
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationWriteService.deleteWaiting(reservationId, memberId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.ALREADY_CONFIRMED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("삭제하려는 회원이 관리자가 아니고, 대기한 회원과 다르면 예외 응답") {
|
||||||
|
every { reservationFinder.findById(reservationId) } returns waitingReservation
|
||||||
|
every {
|
||||||
|
reservationWriter.deleteWaiting(waitingReservation, memberId)
|
||||||
|
} throws ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER)
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationWriteService.deleteWaiting(reservationId, memberId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.NOT_RESERVATION_OWNER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
package roomescape.reservation.business
|
||||||
|
|
||||||
|
import io.kotest.assertions.assertSoftly
|
||||||
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.collections.shouldHaveSize
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import roomescape.reservation.exception.ReservationErrorCode
|
||||||
|
import roomescape.reservation.exception.ReservationException
|
||||||
|
import roomescape.reservation.implement.ReservationFinder
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||||
|
import roomescape.reservation.web.MyReservationRetrieveResponse
|
||||||
|
import roomescape.util.MemberFixture
|
||||||
|
import roomescape.util.ReservationFixture
|
||||||
|
import roomescape.util.ThemeFixture
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
class ReservationQueryServiceTest : FunSpec({
|
||||||
|
|
||||||
|
val reservationFinder: ReservationFinder = mockk()
|
||||||
|
val reservationFindService = ReservationFindService(reservationFinder)
|
||||||
|
|
||||||
|
context("findReservations") {
|
||||||
|
test("정상 응답") {
|
||||||
|
val confirmedReservations = listOf(
|
||||||
|
ReservationFixture.create(status = ReservationStatus.CONFIRMED),
|
||||||
|
ReservationFixture.create(status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)
|
||||||
|
)
|
||||||
|
every {
|
||||||
|
reservationFinder.findAllByStatuses(*ReservationStatus.confirmedStatus())
|
||||||
|
} returns confirmedReservations
|
||||||
|
|
||||||
|
val response = reservationFindService.findReservations()
|
||||||
|
|
||||||
|
assertSoftly(response.reservations) {
|
||||||
|
this shouldHaveSize 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("findAllWaiting") {
|
||||||
|
test("정상 응답") {
|
||||||
|
val waitingReservations = listOf(
|
||||||
|
ReservationFixture.create(status = ReservationStatus.WAITING),
|
||||||
|
ReservationFixture.create(status = ReservationStatus.WAITING)
|
||||||
|
)
|
||||||
|
every {
|
||||||
|
reservationFinder.findAllByStatuses(ReservationStatus.WAITING)
|
||||||
|
} returns waitingReservations
|
||||||
|
|
||||||
|
val response = reservationFindService.findAllWaiting()
|
||||||
|
|
||||||
|
assertSoftly(response.reservations) {
|
||||||
|
this shouldHaveSize 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("findReservationsByMemberId") {
|
||||||
|
val memberId = 1L
|
||||||
|
test("정상 응답") {
|
||||||
|
val myReservations = listOf<MyReservationRetrieveResponse>(mockk(), mockk())
|
||||||
|
|
||||||
|
every {
|
||||||
|
reservationFinder.findAllByMemberId(memberId)
|
||||||
|
} returns myReservations
|
||||||
|
|
||||||
|
val response = reservationFindService.findReservationsByMemberId(memberId)
|
||||||
|
|
||||||
|
response.reservations shouldHaveSize 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("searchReservations") {
|
||||||
|
val themeId = 1L
|
||||||
|
val memberId = 1L
|
||||||
|
val startFrom = LocalDate.now()
|
||||||
|
val endAt = LocalDate.now().plusDays(1)
|
||||||
|
|
||||||
|
test("정상 응답") {
|
||||||
|
val searchedReservations = listOf(
|
||||||
|
ReservationFixture.create(
|
||||||
|
theme = ThemeFixture.create(themeId),
|
||||||
|
member = MemberFixture.create(memberId),
|
||||||
|
date = startFrom
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
every {
|
||||||
|
reservationFinder.searchReservations(themeId, memberId, startFrom, endAt)
|
||||||
|
} returns searchedReservations
|
||||||
|
|
||||||
|
val response = reservationFindService.searchReservations(themeId, memberId, startFrom, endAt)
|
||||||
|
|
||||||
|
assertSoftly(response.reservations) {
|
||||||
|
this shouldHaveSize 1
|
||||||
|
this[0].theme.id shouldBe themeId
|
||||||
|
this[0].member.id shouldBe memberId
|
||||||
|
this[0].date shouldBe startFrom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("종료 날짜가 시작 날짜 이전이면 예외 응답") {
|
||||||
|
val invalidEndAt = startFrom.minusDays(1)
|
||||||
|
every {
|
||||||
|
reservationFinder.searchReservations(themeId, memberId, startFrom, invalidEndAt)
|
||||||
|
} throws ReservationException(ReservationErrorCode.INVALID_SEARCH_DATE_RANGE)
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationFindService.searchReservations(themeId, memberId, startFrom, invalidEndAt)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.INVALID_SEARCH_DATE_RANGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -1,287 +0,0 @@
|
|||||||
package roomescape.reservation.business
|
|
||||||
|
|
||||||
import io.kotest.assertions.throwables.shouldThrow
|
|
||||||
import io.kotest.core.spec.style.FunSpec
|
|
||||||
import io.kotest.matchers.shouldBe
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.mockk
|
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
|
||||||
import roomescape.member.business.MemberService
|
|
||||||
import roomescape.member.infrastructure.persistence.Role
|
|
||||||
import roomescape.reservation.exception.ReservationErrorCode
|
|
||||||
import roomescape.reservation.exception.ReservationException
|
|
||||||
import roomescape.reservation.infrastructure.persistence.ReservationRepository
|
|
||||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
|
||||||
import roomescape.theme.business.ThemeService
|
|
||||||
import roomescape.time.business.TimeService
|
|
||||||
import roomescape.util.MemberFixture
|
|
||||||
import roomescape.util.ReservationFixture
|
|
||||||
import roomescape.util.TsidFactory
|
|
||||||
import roomescape.util.TimeFixture
|
|
||||||
import java.time.LocalDate
|
|
||||||
import java.time.LocalTime
|
|
||||||
|
|
||||||
class ReservationServiceTest : FunSpec({
|
|
||||||
|
|
||||||
val reservationRepository: ReservationRepository = mockk()
|
|
||||||
val timeService: TimeService = mockk()
|
|
||||||
val memberService: MemberService = mockk()
|
|
||||||
val themeService: ThemeService = mockk()
|
|
||||||
val reservationService = ReservationService(
|
|
||||||
TsidFactory,
|
|
||||||
reservationRepository,
|
|
||||||
timeService,
|
|
||||||
memberService,
|
|
||||||
themeService
|
|
||||||
)
|
|
||||||
|
|
||||||
context("예약을 추가할 때") {
|
|
||||||
test("이미 예약이 있으면 예외를 던진다.") {
|
|
||||||
every {
|
|
||||||
reservationRepository.exists(any())
|
|
||||||
} returns true
|
|
||||||
|
|
||||||
val reservationRequest = ReservationFixture.createRequest()
|
|
||||||
|
|
||||||
shouldThrow<ReservationException> {
|
|
||||||
reservationService.createConfirmedReservation(reservationRequest, 1L)
|
|
||||||
}.also {
|
|
||||||
it.errorCode shouldBe ReservationErrorCode.RESERVATION_DUPLICATED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context("날짜, 시간이 잘못 입력되면 예외를 던진다.") {
|
|
||||||
every {
|
|
||||||
reservationRepository.exists(any())
|
|
||||||
} returns false
|
|
||||||
|
|
||||||
every {
|
|
||||||
themeService.findById(any())
|
|
||||||
} returns mockk()
|
|
||||||
|
|
||||||
every {
|
|
||||||
memberService.findById(any())
|
|
||||||
} returns mockk()
|
|
||||||
|
|
||||||
|
|
||||||
test("지난 날짜이면 예외를 던진다.") {
|
|
||||||
val reservationRequest = ReservationFixture.createRequest().copy(
|
|
||||||
date = LocalDate.now().minusDays(1)
|
|
||||||
)
|
|
||||||
|
|
||||||
every {
|
|
||||||
timeService.findById(any())
|
|
||||||
} returns TimeFixture.create()
|
|
||||||
|
|
||||||
shouldThrow<ReservationException> {
|
|
||||||
reservationService.createConfirmedReservation(reservationRequest, 1L)
|
|
||||||
}.also {
|
|
||||||
it.errorCode shouldBe ReservationErrorCode.PAST_REQUEST_DATETIME
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test("지난 시간이면 예외를 던진다.") {
|
|
||||||
val reservationRequest = ReservationFixture.createRequest().copy(
|
|
||||||
date = LocalDate.now(),
|
|
||||||
)
|
|
||||||
|
|
||||||
every {
|
|
||||||
timeService.findById(reservationRequest.timeId)
|
|
||||||
} returns TimeFixture.create(
|
|
||||||
startAt = LocalTime.now().minusMinutes(1)
|
|
||||||
)
|
|
||||||
|
|
||||||
shouldThrow<ReservationException> {
|
|
||||||
reservationService.createConfirmedReservation(reservationRequest, 1L)
|
|
||||||
}.also {
|
|
||||||
it.errorCode shouldBe ReservationErrorCode.PAST_REQUEST_DATETIME
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context("예약 대기를 걸 때") {
|
|
||||||
test("이미 예약한 회원이 같은 날짜와 테마로 대기를 걸면 예외를 던진다.") {
|
|
||||||
val reservationRequest = ReservationFixture.createRequest().copy(
|
|
||||||
date = LocalDate.now(),
|
|
||||||
themeId = 1L,
|
|
||||||
timeId = 1L,
|
|
||||||
)
|
|
||||||
|
|
||||||
every {
|
|
||||||
reservationRepository.exists(any())
|
|
||||||
} returns true
|
|
||||||
|
|
||||||
shouldThrow<ReservationException> {
|
|
||||||
val waitingRequest = ReservationFixture.createWaitingRequest(
|
|
||||||
date = reservationRequest.date,
|
|
||||||
themeId = reservationRequest.themeId,
|
|
||||||
timeId = reservationRequest.timeId
|
|
||||||
)
|
|
||||||
reservationService.createWaiting(waitingRequest, 1L)
|
|
||||||
}.also {
|
|
||||||
it.errorCode shouldBe ReservationErrorCode.ALREADY_RESERVE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context("예약 대기를 취소할 때") {
|
|
||||||
val reservationId = 1L
|
|
||||||
val member = MemberFixture.create(id = 1L, role = Role.MEMBER)
|
|
||||||
test("예약을 찾을 수 없으면 예외를 던진다.") {
|
|
||||||
every {
|
|
||||||
reservationRepository.findByIdOrNull(reservationId)
|
|
||||||
} returns null
|
|
||||||
|
|
||||||
shouldThrow<ReservationException> {
|
|
||||||
reservationService.deleteWaiting(reservationId, member.id!!)
|
|
||||||
}.also {
|
|
||||||
it.errorCode shouldBe ReservationErrorCode.RESERVATION_NOT_FOUND
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test("대기중인 해당 예약이 이미 확정된 상태라면 예외를 던진다.") {
|
|
||||||
val alreadyConfirmed = ReservationFixture.create(
|
|
||||||
id = reservationId,
|
|
||||||
status = ReservationStatus.CONFIRMED
|
|
||||||
)
|
|
||||||
every {
|
|
||||||
reservationRepository.findByIdOrNull(reservationId)
|
|
||||||
} returns alreadyConfirmed
|
|
||||||
|
|
||||||
shouldThrow<ReservationException> {
|
|
||||||
reservationService.deleteWaiting(reservationId, member.id!!)
|
|
||||||
}.also {
|
|
||||||
it.errorCode shouldBe ReservationErrorCode.ALREADY_CONFIRMED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test("타인의 대기를 취소하려고 하면 예외를 던진다.") {
|
|
||||||
val otherMembersWaiting = ReservationFixture.create(
|
|
||||||
id = reservationId,
|
|
||||||
member = MemberFixture.create(id = member.id!! + 1L),
|
|
||||||
status = ReservationStatus.WAITING
|
|
||||||
)
|
|
||||||
|
|
||||||
every {
|
|
||||||
reservationRepository.findByIdOrNull(reservationId)
|
|
||||||
} returns otherMembersWaiting
|
|
||||||
|
|
||||||
shouldThrow<ReservationException> {
|
|
||||||
reservationService.deleteWaiting(reservationId, member.id!!)
|
|
||||||
}.also {
|
|
||||||
it.errorCode shouldBe ReservationErrorCode.NOT_RESERVATION_OWNER
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context("예약을 조회할 때") {
|
|
||||||
test("종료 날짜가 시작 날짜보다 이전이면 예외를 던진다.") {
|
|
||||||
val startFrom = LocalDate.now()
|
|
||||||
val endAt = startFrom.minusDays(1)
|
|
||||||
|
|
||||||
shouldThrow<ReservationException> {
|
|
||||||
reservationService.searchReservations(
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
startFrom,
|
|
||||||
endAt
|
|
||||||
)
|
|
||||||
}.also {
|
|
||||||
it.errorCode shouldBe ReservationErrorCode.INVALID_SEARCH_DATE_RANGE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context("대기중인 예약을 승인할 때") {
|
|
||||||
test("관리자가 아니면 예외를 던진다.") {
|
|
||||||
val member = MemberFixture.create(id = 1L, role = Role.MEMBER)
|
|
||||||
|
|
||||||
every {
|
|
||||||
memberService.findById(any())
|
|
||||||
} returns member
|
|
||||||
|
|
||||||
shouldThrow<ReservationException> {
|
|
||||||
reservationService.confirmWaiting(1L, member.id!!)
|
|
||||||
}.also {
|
|
||||||
it.errorCode shouldBe ReservationErrorCode.NO_PERMISSION
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test("이미 확정된 예약이 있으면 예외를 던진다.") {
|
|
||||||
val member = MemberFixture.create(id = 1L, role = Role.ADMIN)
|
|
||||||
val reservationId = 1L
|
|
||||||
|
|
||||||
every {
|
|
||||||
memberService.findById(any())
|
|
||||||
} returns member
|
|
||||||
|
|
||||||
every {
|
|
||||||
reservationRepository.isExistConfirmedReservation(reservationId)
|
|
||||||
} returns true
|
|
||||||
|
|
||||||
shouldThrow<ReservationException> {
|
|
||||||
reservationService.confirmWaiting(reservationId, member.id!!)
|
|
||||||
}.also {
|
|
||||||
it.errorCode shouldBe ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context("대기중인 예약을 거절할 때") {
|
|
||||||
test("관리자가 아니면 예외를 던진다.") {
|
|
||||||
val member = MemberFixture.create(id = 1L, role = Role.MEMBER)
|
|
||||||
|
|
||||||
every {
|
|
||||||
memberService.findById(any())
|
|
||||||
} returns member
|
|
||||||
|
|
||||||
shouldThrow<ReservationException> {
|
|
||||||
reservationService.rejectWaiting(1L, member.id!!)
|
|
||||||
}.also {
|
|
||||||
it.errorCode shouldBe ReservationErrorCode.NO_PERMISSION
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test("예약을 찾을 수 없으면 예외를 던진다.") {
|
|
||||||
val member = MemberFixture.create(id = 1L, role = Role.ADMIN)
|
|
||||||
val reservationId = 1L
|
|
||||||
|
|
||||||
every {
|
|
||||||
memberService.findById(member.id!!)
|
|
||||||
} returns member
|
|
||||||
|
|
||||||
every {
|
|
||||||
reservationRepository.findByIdOrNull(reservationId)
|
|
||||||
} returns null
|
|
||||||
|
|
||||||
shouldThrow<ReservationException> {
|
|
||||||
reservationService.rejectWaiting(reservationId, member.id!!)
|
|
||||||
}.also {
|
|
||||||
it.errorCode shouldBe ReservationErrorCode.RESERVATION_NOT_FOUND
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test("이미 확정된 예약이면 예외를 던진다.") {
|
|
||||||
val member = MemberFixture.create(id = 1L, role = Role.ADMIN)
|
|
||||||
val reservation = ReservationFixture.create(
|
|
||||||
id = 1L,
|
|
||||||
status = ReservationStatus.CONFIRMED
|
|
||||||
)
|
|
||||||
|
|
||||||
every {
|
|
||||||
memberService.findById(member.id!!)
|
|
||||||
} returns member
|
|
||||||
|
|
||||||
every {
|
|
||||||
reservationRepository.findByIdOrNull(reservation.id!!)
|
|
||||||
} returns reservation
|
|
||||||
|
|
||||||
shouldThrow<ReservationException> {
|
|
||||||
reservationService.rejectWaiting(reservation.id!!, member.id!!)
|
|
||||||
}.also {
|
|
||||||
it.errorCode shouldBe ReservationErrorCode.ALREADY_CONFIRMED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@ -13,16 +13,16 @@ import roomescape.payment.web.PaymentCancelRequest
|
|||||||
import roomescape.payment.web.toCreateResponse
|
import roomescape.payment.web.toCreateResponse
|
||||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||||
|
import roomescape.reservation.web.ReservationCreateResponse
|
||||||
import roomescape.reservation.web.ReservationCreateWithPaymentRequest
|
import roomescape.reservation.web.ReservationCreateWithPaymentRequest
|
||||||
import roomescape.reservation.web.ReservationRetrieveResponse
|
|
||||||
import roomescape.util.*
|
import roomescape.util.*
|
||||||
|
|
||||||
class ReservationWithPaymentServiceTest : FunSpec({
|
class ReservationWithPaymentServiceTest : FunSpec({
|
||||||
val reservationService: ReservationService = mockk()
|
val reservationService: ReservationWriteService = mockk()
|
||||||
val paymentService: PaymentService = mockk()
|
val paymentService: PaymentService = mockk()
|
||||||
|
|
||||||
val reservationWithPaymentService = ReservationWithPaymentService(
|
val reservationWithPaymentService = ReservationWithPaymentService(
|
||||||
reservationService = reservationService,
|
reservationWriteService = reservationService,
|
||||||
paymentService = paymentService
|
paymentService = paymentService
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -48,16 +48,16 @@ class ReservationWithPaymentServiceTest : FunSpec({
|
|||||||
context("addReservationWithPayment") {
|
context("addReservationWithPayment") {
|
||||||
test("예약 및 결제 정보를 저장한다.") {
|
test("예약 및 결제 정보를 저장한다.") {
|
||||||
every {
|
every {
|
||||||
reservationService.createConfirmedReservation(reservationCreateWithPaymentRequest, memberId)
|
reservationService.createReservationWithPayment(reservationCreateWithPaymentRequest, memberId)
|
||||||
} returns reservationEntity
|
} returns reservationEntity
|
||||||
|
|
||||||
every {
|
every {
|
||||||
paymentService.createPayment(paymentApproveResponse, reservationEntity)
|
paymentService.createPayment(paymentApproveResponse, reservationEntity)
|
||||||
} returns paymentEntity.toCreateResponse()
|
} returns paymentEntity.toCreateResponse()
|
||||||
|
|
||||||
val result: ReservationRetrieveResponse = reservationWithPaymentService.createReservationAndPayment(
|
val result: ReservationCreateResponse = reservationWithPaymentService.createReservationAndPayment(
|
||||||
request = reservationCreateWithPaymentRequest,
|
request = reservationCreateWithPaymentRequest,
|
||||||
paymentInfo = paymentApproveResponse,
|
approvedPaymentInfo = paymentApproveResponse,
|
||||||
memberId = memberId
|
memberId = memberId
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ class ReservationWithPaymentServiceTest : FunSpec({
|
|||||||
)
|
)
|
||||||
|
|
||||||
every {
|
every {
|
||||||
paymentService.createCanceledPaymentByReservationId(reservationEntity.id!!)
|
paymentService.createCanceledPayment(reservationEntity.id!!)
|
||||||
} returns paymentCancelRequest
|
} returns paymentCancelRequest
|
||||||
|
|
||||||
every {
|
every {
|
||||||
@ -100,7 +100,7 @@ class ReservationWithPaymentServiceTest : FunSpec({
|
|||||||
context("isNotPaidReservation") {
|
context("isNotPaidReservation") {
|
||||||
test("결제된 예약이면 true를 반환한다.") {
|
test("결제된 예약이면 true를 반환한다.") {
|
||||||
every {
|
every {
|
||||||
paymentService.isReservationPaid(reservationEntity.id!!)
|
paymentService.existsByReservationId(reservationEntity.id!!)
|
||||||
} returns false
|
} returns false
|
||||||
|
|
||||||
val result: Boolean = reservationWithPaymentService.isNotPaidReservation(reservationEntity.id!!)
|
val result: Boolean = reservationWithPaymentService.isNotPaidReservation(reservationEntity.id!!)
|
||||||
@ -110,7 +110,7 @@ class ReservationWithPaymentServiceTest : FunSpec({
|
|||||||
|
|
||||||
test("결제되지 않은 예약이면 false를 반환한다.") {
|
test("결제되지 않은 예약이면 false를 반환한다.") {
|
||||||
every {
|
every {
|
||||||
paymentService.isReservationPaid(reservationEntity.id!!)
|
paymentService.existsByReservationId(reservationEntity.id!!)
|
||||||
} returns true
|
} returns true
|
||||||
|
|
||||||
val result: Boolean = reservationWithPaymentService.isNotPaidReservation(reservationEntity.id!!)
|
val result: Boolean = reservationWithPaymentService.isNotPaidReservation(reservationEntity.id!!)
|
||||||
|
|||||||
@ -0,0 +1,64 @@
|
|||||||
|
package roomescape.reservation.implement
|
||||||
|
|
||||||
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.spyk
|
||||||
|
import io.mockk.verify
|
||||||
|
import org.springframework.data.jpa.domain.Specification
|
||||||
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
|
import roomescape.reservation.exception.ReservationErrorCode
|
||||||
|
import roomescape.reservation.exception.ReservationException
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationRepository
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationSearchSpecification
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||||
|
import roomescape.time.exception.TimeErrorCode
|
||||||
|
import roomescape.time.exception.TimeException
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
class ReservationFinderTest : FunSpec({
|
||||||
|
val reservationRepository: ReservationRepository = mockk()
|
||||||
|
val reservationValidator = ReservationValidator(reservationRepository)
|
||||||
|
|
||||||
|
val reservationFinder = ReservationFinder(reservationRepository, reservationValidator)
|
||||||
|
|
||||||
|
context("findById") {
|
||||||
|
val reservationId = 1L
|
||||||
|
test("동일한 ID인 시간을 찾아 응답한다.") {
|
||||||
|
every {
|
||||||
|
reservationRepository.findByIdOrNull(reservationId)
|
||||||
|
} returns mockk()
|
||||||
|
|
||||||
|
reservationFinder.findById(reservationId)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
reservationRepository.findByIdOrNull(reservationId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("동일한 ID인 시간이 없으면 실패한다.") {
|
||||||
|
every {
|
||||||
|
reservationRepository.findByIdOrNull(reservationId)
|
||||||
|
} returns null
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationFinder.findById(reservationId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.RESERVATION_NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("searchReservations") {
|
||||||
|
test("시작 날짜가 종료 날짜 이전이면 실패한다.") {
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationFinder.searchReservations(1L, 1L, LocalDate.now(), LocalDate.now().minusDays(1))
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.INVALID_SEARCH_DATE_RANGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -0,0 +1,170 @@
|
|||||||
|
package roomescape.reservation.implement
|
||||||
|
|
||||||
|
import io.kotest.assertions.throwables.shouldNotThrow
|
||||||
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.springframework.data.jpa.domain.Specification
|
||||||
|
import roomescape.member.infrastructure.persistence.Role
|
||||||
|
import roomescape.reservation.exception.ReservationErrorCode
|
||||||
|
import roomescape.reservation.exception.ReservationException
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationRepository
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||||
|
import roomescape.util.MemberFixture
|
||||||
|
import roomescape.util.ReservationFixture
|
||||||
|
import roomescape.util.ThemeFixture
|
||||||
|
import roomescape.util.TimeFixture
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalTime
|
||||||
|
|
||||||
|
class ReservationValidatorTest : FunSpec({
|
||||||
|
val reservationRepository: ReservationRepository = mockk()
|
||||||
|
|
||||||
|
val reservationValidator = ReservationValidator(reservationRepository)
|
||||||
|
|
||||||
|
context("validateIsNotPast") {
|
||||||
|
val today = LocalDate.now()
|
||||||
|
val now = LocalTime.now()
|
||||||
|
|
||||||
|
test("입력된 날짜가 오늘 이전이면 예외를 던진다.") {
|
||||||
|
val requestDate = today.minusDays(1)
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationValidator.validateIsPast(requestDate, now)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.PAST_REQUEST_DATETIME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("오늘 날짜라도 시간이 지났다면 예외를 던진다.") {
|
||||||
|
val requestTime = now.minusMinutes(1)
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationValidator.validateIsPast(today, requestTime)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.PAST_REQUEST_DATETIME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("validateSearchDateRange") {
|
||||||
|
test("시작 날짜만 입력되면 종료한다.") {
|
||||||
|
shouldNotThrow<ReservationException> {
|
||||||
|
reservationValidator.validateSearchDateRange(LocalDate.now(), null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("종료 날짜만 입력되면 종료한다.") {
|
||||||
|
shouldNotThrow<ReservationException> {
|
||||||
|
reservationValidator.validateSearchDateRange(null, LocalDate.now())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("두 날짜가 같으면 종료한다.") {
|
||||||
|
shouldNotThrow<ReservationException> {
|
||||||
|
reservationValidator.validateSearchDateRange(LocalDate.now(), LocalDate.now())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("종료 날짜가 시작 날짜 이전이면 예외를 던진다.") {
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationValidator.validateSearchDateRange(LocalDate.now(), LocalDate.now().minusDays(1))
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.INVALID_SEARCH_DATE_RANGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("validateIsAlreadyExists") {
|
||||||
|
test("동일한 날짜, 시간, 테마를 가지는 예약이 있으면 예외를 던진다.") {
|
||||||
|
every {
|
||||||
|
reservationRepository.exists(any<Specification<ReservationEntity>>())
|
||||||
|
} returns true
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationValidator.validateIsAlreadyExists(
|
||||||
|
LocalDate.now(),
|
||||||
|
TimeFixture.create(),
|
||||||
|
ThemeFixture.create()
|
||||||
|
)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.RESERVATION_DUPLICATED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("validateMemberAlreadyReserve") {
|
||||||
|
test("회원이 동일한 날짜, 시간, 테마인 예약(대기)를 이미 했다면 예외를 던진다.") {
|
||||||
|
every {
|
||||||
|
reservationRepository.exists(any<Specification<ReservationEntity>>())
|
||||||
|
} returns true
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationValidator.validateMemberAlreadyReserve(1L, 1L, LocalDate.now(), 1L)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.ALREADY_RESERVE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("validateIsWaiting") {
|
||||||
|
test("예약 상태가 WAITING이 아니면 예외를 던진다.") {
|
||||||
|
ReservationStatus.confirmedStatus().forEach { status ->
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
val reservation = ReservationFixture.create(status = status)
|
||||||
|
reservationValidator.validateIsWaiting(reservation)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.ALREADY_CONFIRMED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("validateCreateAuthority") {
|
||||||
|
test("관리자가 아니면 예외를 던진다.") {
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationValidator.validateCreateAuthority(MemberFixture.user())
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.NO_PERMISSION
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("validateDeleteAuthority") {
|
||||||
|
test("입력된 회원이 관리자이면 종료한다.") {
|
||||||
|
shouldNotThrow<ReservationException> {
|
||||||
|
reservationValidator.validateDeleteAuthority(mockk(), MemberFixture.admin())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("입력된 회원이 관리자가 아니고, 예약한 회원과 다른 회원이면 예외를 던진다.") {
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationValidator.validateDeleteAuthority(
|
||||||
|
ReservationFixture.create(member = MemberFixture.create(id = 1L)),
|
||||||
|
MemberFixture.create(id = 2L, role = Role.MEMBER)
|
||||||
|
)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.NOT_RESERVATION_OWNER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("validateAlreadyConfirmed") {
|
||||||
|
val reservationId = 1L
|
||||||
|
|
||||||
|
test("입력된 ID의 예약과 동일한 날짜, 시간, 테마를 가지는 다른 확정 예약이 있으면 예외를 던진다.") {
|
||||||
|
every {
|
||||||
|
reservationRepository.isExistConfirmedReservation(reservationId)
|
||||||
|
} returns true
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationValidator.validateAlreadyConfirmed(reservationId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -0,0 +1,246 @@
|
|||||||
|
package roomescape.reservation.implement
|
||||||
|
|
||||||
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import roomescape.member.exception.MemberErrorCode
|
||||||
|
import roomescape.member.exception.MemberException
|
||||||
|
import roomescape.member.implement.MemberFinder
|
||||||
|
import roomescape.reservation.exception.ReservationErrorCode
|
||||||
|
import roomescape.reservation.exception.ReservationException
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationRepository
|
||||||
|
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||||
|
import roomescape.theme.exception.ThemeErrorCode
|
||||||
|
import roomescape.theme.exception.ThemeException
|
||||||
|
import roomescape.theme.implement.ThemeFinder
|
||||||
|
import roomescape.time.exception.TimeErrorCode
|
||||||
|
import roomescape.time.exception.TimeException
|
||||||
|
import roomescape.time.implement.TimeFinder
|
||||||
|
import roomescape.util.MemberFixture
|
||||||
|
import roomescape.util.ThemeFixture
|
||||||
|
import roomescape.util.TimeFixture
|
||||||
|
import roomescape.util.TsidFactory
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalTime
|
||||||
|
|
||||||
|
class ReservationWriterTest : FunSpec({
|
||||||
|
|
||||||
|
val reservationValidator: ReservationValidator = mockk()
|
||||||
|
val reservationRepository: ReservationRepository = mockk()
|
||||||
|
val memberFinder: MemberFinder = mockk()
|
||||||
|
val timeFinder: TimeFinder = mockk()
|
||||||
|
val themeFinder: ThemeFinder = mockk()
|
||||||
|
|
||||||
|
val reservationWriter = ReservationWriter(
|
||||||
|
reservationValidator, reservationRepository, memberFinder, timeFinder, themeFinder, TsidFactory
|
||||||
|
)
|
||||||
|
|
||||||
|
context("create") {
|
||||||
|
val today = LocalDate.now()
|
||||||
|
val timeId = 1L
|
||||||
|
val themeId = 1L
|
||||||
|
val memberId = 1L
|
||||||
|
val status = ReservationStatus.CONFIRMED
|
||||||
|
val requesterId = 1L
|
||||||
|
|
||||||
|
test("시간을 찾을 수 없으면 실패한다.") {
|
||||||
|
every {
|
||||||
|
timeFinder.findById(any())
|
||||||
|
} throws TimeException(TimeErrorCode.TIME_NOT_FOUND)
|
||||||
|
|
||||||
|
shouldThrow<TimeException> {
|
||||||
|
reservationWriter.create(today, timeId, themeId, memberId, status, requesterId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe TimeErrorCode.TIME_NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("이전 날짜이면 실패한다.") {
|
||||||
|
every {
|
||||||
|
timeFinder.findById(timeId)
|
||||||
|
} returns TimeFixture.create(id = timeId, startAt = LocalTime.now().plusHours(1))
|
||||||
|
|
||||||
|
every {
|
||||||
|
reservationValidator.validateIsPast(any(), any())
|
||||||
|
} throws ReservationException(ReservationErrorCode.PAST_REQUEST_DATETIME)
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationWriter.create(today.minusDays(1), timeId, themeId, memberId, status, requesterId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.PAST_REQUEST_DATETIME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("테마를 찾을 수 없으면 실패한다.") {
|
||||||
|
every { timeFinder.findById(timeId) } returns TimeFixture.create(id = timeId)
|
||||||
|
every { reservationValidator.validateIsPast(any(), any()) } returns Unit
|
||||||
|
|
||||||
|
every {
|
||||||
|
themeFinder.findById(themeId)
|
||||||
|
} throws ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
|
||||||
|
|
||||||
|
shouldThrow<ThemeException> {
|
||||||
|
reservationWriter.create(today.plusDays(1), timeId, themeId, memberId, status, requesterId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ThemeErrorCode.THEME_NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("회원을 찾을 수 없으면 실패한다.") {
|
||||||
|
every { timeFinder.findById(timeId) } returns TimeFixture.create(id = timeId)
|
||||||
|
every { reservationValidator.validateIsPast(any(), any()) } returns Unit
|
||||||
|
every { themeFinder.findById(themeId) } returns ThemeFixture.create(id = themeId)
|
||||||
|
|
||||||
|
every {
|
||||||
|
memberFinder.findById(memberId)
|
||||||
|
} throws MemberException(MemberErrorCode.MEMBER_NOT_FOUND)
|
||||||
|
|
||||||
|
shouldThrow<MemberException> {
|
||||||
|
reservationWriter.create(today.plusDays(1), timeId, themeId, memberId, status, requesterId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe MemberErrorCode.MEMBER_NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("이미 예약이 있는 회원이 대기를 추가하면 실패한다.") {
|
||||||
|
every { timeFinder.findById(timeId) } returns TimeFixture.create(id = timeId)
|
||||||
|
every { reservationValidator.validateIsPast(any(), any()) } returns Unit
|
||||||
|
every { themeFinder.findById(themeId) } returns ThemeFixture.create(id = themeId)
|
||||||
|
every { memberFinder.findById(memberId) } returns MemberFixture.create(id = memberId)
|
||||||
|
|
||||||
|
every {
|
||||||
|
reservationValidator.validateMemberAlreadyReserve(themeId, timeId, today, memberId)
|
||||||
|
} throws ReservationException(ReservationErrorCode.ALREADY_RESERVE)
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationWriter.create(today, timeId, themeId, memberId, status = ReservationStatus.WAITING, memberId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.ALREADY_RESERVE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("동일한 날짜, 시간, 테마인 예약이 이미 있으면 실패한다.") {
|
||||||
|
every { timeFinder.findById(timeId) } returns TimeFixture.create(id = timeId)
|
||||||
|
every { reservationValidator.validateIsPast(any(), any()) } returns Unit
|
||||||
|
every { themeFinder.findById(themeId) } returns ThemeFixture.create(id = themeId)
|
||||||
|
every { memberFinder.findById(memberId) } returns MemberFixture.create(id = memberId)
|
||||||
|
|
||||||
|
every {
|
||||||
|
reservationValidator.validateIsAlreadyExists(today, any(), any())
|
||||||
|
} throws ReservationException(ReservationErrorCode.RESERVATION_DUPLICATED)
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationWriter.create(today, timeId, themeId, memberId, status, memberId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.RESERVATION_DUPLICATED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("예약하려는 회원과 신청한 회원이 다를 때, 신청한 회원을 찾을 수 없으면 실패한다.") {
|
||||||
|
every { timeFinder.findById(timeId) } returns TimeFixture.create(id = timeId)
|
||||||
|
every { reservationValidator.validateIsPast(any(), any()) } returns Unit
|
||||||
|
every { themeFinder.findById(themeId) } returns ThemeFixture.create(id = themeId)
|
||||||
|
every { memberFinder.findById(memberId) } returns MemberFixture.create(id = memberId)
|
||||||
|
every { reservationValidator.validateIsAlreadyExists(today, any(), any()) } returns Unit
|
||||||
|
|
||||||
|
every {
|
||||||
|
memberFinder.findById(memberId + 1)
|
||||||
|
} throws MemberException(MemberErrorCode.MEMBER_NOT_FOUND)
|
||||||
|
|
||||||
|
shouldThrow<MemberException> {
|
||||||
|
reservationWriter.create(today, timeId, themeId, memberId = memberId, status, requesterId = (memberId + 1))
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe MemberErrorCode.MEMBER_NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("예약하려는 회원과 신청한 회원이 다를 때, 신청한 회원이 관리자가 아니면 실패한다.") {
|
||||||
|
every { timeFinder.findById(timeId) } returns TimeFixture.create(id = timeId)
|
||||||
|
every { reservationValidator.validateIsPast(any(), any()) } returns Unit
|
||||||
|
every { themeFinder.findById(themeId) } returns ThemeFixture.create(id = themeId)
|
||||||
|
every { memberFinder.findById(memberId) } returns MemberFixture.create(id = memberId)
|
||||||
|
every { reservationValidator.validateIsAlreadyExists(today, any(), any()) } returns Unit
|
||||||
|
|
||||||
|
every {
|
||||||
|
memberFinder.findById(memberId + 1)
|
||||||
|
} returns MemberFixture.create(id = memberId + 1)
|
||||||
|
|
||||||
|
every {
|
||||||
|
reservationValidator.validateCreateAuthority(any())
|
||||||
|
} throws ReservationException(ReservationErrorCode.NO_PERMISSION)
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationWriter.create(today, timeId, themeId, memberId = memberId, status, requesterId = (memberId + 1))
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.NO_PERMISSION
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("deleteWaiting") {
|
||||||
|
val reservation: ReservationEntity = mockk()
|
||||||
|
val requesterId = 1L
|
||||||
|
|
||||||
|
test("대기 상태가 아니면 실패한다.") {
|
||||||
|
every {
|
||||||
|
reservationValidator.validateIsWaiting(any())
|
||||||
|
} throws ReservationException(ReservationErrorCode.ALREADY_CONFIRMED)
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationWriter.deleteWaiting(reservation, requesterId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.ALREADY_CONFIRMED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("삭제하려는 회원이 관리자가 아니고, 예약한 회원과 다르면 실패한다.") {
|
||||||
|
every { reservationValidator.validateIsWaiting(any()) } returns Unit
|
||||||
|
every {
|
||||||
|
reservationValidator.validateDeleteAuthority(any(), any())
|
||||||
|
} throws ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER)
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationWriter.deleteWaiting(reservation, requesterId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.NOT_RESERVATION_OWNER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("deleteConfirm") {
|
||||||
|
val reservation: ReservationEntity = mockk()
|
||||||
|
val requesterId = 1L
|
||||||
|
|
||||||
|
test("삭제하려는 회원이 관리자가 아니고, 예약한 회원과 다르면 실패한다.") {
|
||||||
|
every { reservationValidator.validateIsWaiting(any()) } returns Unit
|
||||||
|
every {
|
||||||
|
reservationValidator.validateDeleteAuthority(any(), any())
|
||||||
|
} throws ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER)
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationWriter.deleteConfirmed(reservation, requesterId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.NOT_RESERVATION_OWNER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("confirm") {
|
||||||
|
val reservationId = 1L
|
||||||
|
|
||||||
|
test("승인하려는 대기와 같은 날짜,시간,테마를 가진 확정 예약이 있으면 실패한다.") {
|
||||||
|
every {
|
||||||
|
reservationValidator.validateAlreadyConfirmed(reservationId)
|
||||||
|
} throws ReservationException(ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS)
|
||||||
|
|
||||||
|
shouldThrow<ReservationException> {
|
||||||
|
reservationWriter.confirm(reservationId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -81,6 +81,21 @@ class ReservationSearchSpecificationTest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"여러 상태를 입력받아 같은 상태안 예약을 조회한다." {
|
||||||
|
val spec = ReservationSearchSpecification()
|
||||||
|
.status(
|
||||||
|
ReservationStatus.CONFIRMED,
|
||||||
|
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED,
|
||||||
|
ReservationStatus.WAITING
|
||||||
|
).build()
|
||||||
|
|
||||||
|
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
||||||
|
|
||||||
|
assertSoftly(results) {
|
||||||
|
this shouldHaveSize reservationRepository.findAll().size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
"확정 상태인 예약을 조회한다" {
|
"확정 상태인 예약을 조회한다" {
|
||||||
val spec = ReservationSearchSpecification()
|
val spec = ReservationSearchSpecification()
|
||||||
.confirmed()
|
.confirmed()
|
||||||
|
|||||||
@ -1,87 +1,108 @@
|
|||||||
package roomescape.theme.business
|
package roomescape.theme.business
|
||||||
|
|
||||||
import io.kotest.assertions.assertSoftly
|
import io.kotest.assertions.assertSoftly
|
||||||
|
import io.kotest.assertions.throwables.shouldNotThrow
|
||||||
import io.kotest.assertions.throwables.shouldThrow
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.collections.shouldContainExactly
|
||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
import io.mockk.every
|
import io.mockk.*
|
||||||
import io.mockk.mockk
|
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
|
||||||
import roomescape.theme.exception.ThemeErrorCode
|
import roomescape.theme.exception.ThemeErrorCode
|
||||||
import roomescape.theme.exception.ThemeException
|
import roomescape.theme.exception.ThemeException
|
||||||
|
import roomescape.theme.implement.ThemeFinder
|
||||||
|
import roomescape.theme.implement.ThemeWriter
|
||||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||||
import roomescape.theme.infrastructure.persistence.ThemeRepository
|
|
||||||
import roomescape.theme.web.ThemeCreateRequest
|
import roomescape.theme.web.ThemeCreateRequest
|
||||||
import roomescape.theme.web.ThemeRetrieveResponse
|
import roomescape.theme.web.ThemeCreateResponse
|
||||||
import roomescape.util.TsidFactory
|
|
||||||
import roomescape.util.ThemeFixture
|
import roomescape.util.ThemeFixture
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
class ThemeServiceTest : FunSpec({
|
class ThemeServiceTest : FunSpec({
|
||||||
|
val themeFinder: ThemeFinder = mockk()
|
||||||
|
val themeWriter: ThemeWriter = mockk()
|
||||||
|
|
||||||
val themeRepository: ThemeRepository = mockk()
|
val themeService = ThemeService(themeFinder, themeWriter)
|
||||||
val themeService = ThemeService(TsidFactory, themeRepository)
|
|
||||||
|
|
||||||
context("findThemeById") {
|
context("findThemeById") {
|
||||||
val themeId = 1L
|
val themeId = 1L
|
||||||
test("조회 성공") {
|
|
||||||
|
test("정상 응답") {
|
||||||
val theme: ThemeEntity = ThemeFixture.create(id = themeId)
|
val theme: ThemeEntity = ThemeFixture.create(id = themeId)
|
||||||
every {
|
every {
|
||||||
themeRepository.findByIdOrNull(themeId)
|
themeFinder.findById(themeId)
|
||||||
} returns theme
|
} returns theme
|
||||||
|
|
||||||
theme.id shouldBe themeId
|
theme.id shouldBe themeId
|
||||||
}
|
}
|
||||||
|
|
||||||
test("ID로 테마를 찾을 수 없으면 400 예외를 던진다.") {
|
test("테마를 찾을 수 없으면 예외 응답") {
|
||||||
every {
|
every {
|
||||||
themeRepository.findByIdOrNull(themeId)
|
themeFinder.findById(themeId)
|
||||||
} returns null
|
} throws ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
|
||||||
|
|
||||||
val exception = shouldThrow<ThemeException> {
|
shouldThrow<ThemeException> {
|
||||||
themeService.findById(themeId)
|
themeService.findById(themeId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ThemeErrorCode.THEME_NOT_FOUND
|
||||||
}
|
}
|
||||||
|
|
||||||
exception.errorCode shouldBe ThemeErrorCode.THEME_NOT_FOUND
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context("findAllThemes") {
|
context("findThemes") {
|
||||||
test("모든 테마를 조회한다.") {
|
test("정상 응답") {
|
||||||
val themes = listOf(ThemeFixture.create(id = 1, name = "t1"), ThemeFixture.create(id = 2, name = "t2"))
|
val themes = listOf(
|
||||||
|
ThemeFixture.create(id = 1, name = "t1"),
|
||||||
|
ThemeFixture.create(id = 2, name = "t2")
|
||||||
|
)
|
||||||
|
|
||||||
every {
|
every {
|
||||||
themeRepository.findAll()
|
themeFinder.findAll()
|
||||||
} returns themes
|
} returns themes
|
||||||
|
|
||||||
assertSoftly(themeService.findThemes()) {
|
val response = themeService.findThemes()
|
||||||
this.themes.size shouldBe themes.size
|
|
||||||
this.themes[0].name shouldBe "t1"
|
assertSoftly(response.themes) {
|
||||||
this.themes[1].name shouldBe "t2"
|
this.size shouldBe themes.size
|
||||||
|
it.map { theme -> theme.name } shouldContainExactly themes.map { theme -> theme.name }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context("save") {
|
context("findMostReservedThemes") {
|
||||||
|
test("7일 전 부터 1일 전 까지 조회") {
|
||||||
|
val count = 10
|
||||||
|
val startFrom = slot<LocalDate>()
|
||||||
|
val endAt = slot<LocalDate>()
|
||||||
|
|
||||||
|
every {
|
||||||
|
themeFinder.findMostReservedThemes(count, capture(startFrom), capture(endAt))
|
||||||
|
} returns emptyList()
|
||||||
|
|
||||||
|
themeService.findMostReservedThemes(count)
|
||||||
|
|
||||||
|
startFrom.captured shouldBe LocalDate.now().minusDays(7)
|
||||||
|
endAt.captured shouldBe LocalDate.now().minusDays(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("createTheme") {
|
||||||
val request = ThemeCreateRequest(
|
val request = ThemeCreateRequest(
|
||||||
name = "New Theme",
|
name = "New Theme",
|
||||||
description = "Description",
|
description = "Description",
|
||||||
thumbnail = "http://example.com/thumbnail.jpg"
|
thumbnail = "http://example.com/thumbnail.jpg"
|
||||||
)
|
)
|
||||||
|
|
||||||
test("저장 성공") {
|
test("정상 저장") {
|
||||||
every {
|
every {
|
||||||
themeRepository.existsByName(request.name)
|
themeWriter.create(request.name, request.description, request.thumbnail)
|
||||||
} returns false
|
|
||||||
|
|
||||||
every {
|
|
||||||
themeRepository.save(any())
|
|
||||||
} returns ThemeFixture.create(
|
} returns ThemeFixture.create(
|
||||||
id = 1L,
|
id = 1,
|
||||||
name = request.name,
|
name = request.name,
|
||||||
description = request.description,
|
description = request.description,
|
||||||
thumbnail = request.thumbnail
|
thumbnail = request.thumbnail
|
||||||
)
|
)
|
||||||
|
|
||||||
val response: ThemeRetrieveResponse = themeService.createTheme(request)
|
val response: ThemeCreateResponse = themeService.createTheme(request)
|
||||||
|
|
||||||
assertSoftly(response) {
|
assertSoftly(response) {
|
||||||
this.id shouldBe 1L
|
this.id shouldBe 1L
|
||||||
@ -91,32 +112,51 @@ class ThemeServiceTest : FunSpec({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("테마 이름이 중복되면 409 예외를 던진다.") {
|
test("중복된 이름이 있으면 예외 응답") {
|
||||||
every {
|
every {
|
||||||
themeRepository.existsByName(request.name)
|
themeWriter.create(request.name, request.description, request.thumbnail)
|
||||||
} returns true
|
} throws ThemeException(ThemeErrorCode.THEME_NAME_DUPLICATED)
|
||||||
|
|
||||||
val exception = shouldThrow<ThemeException> {
|
shouldThrow<ThemeException> {
|
||||||
themeService.createTheme(request)
|
themeService.createTheme(request)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ThemeErrorCode.THEME_NAME_DUPLICATED
|
||||||
}
|
}
|
||||||
|
|
||||||
exception.errorCode shouldBe ThemeErrorCode.THEME_NAME_DUPLICATED
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context("deleteById") {
|
context("deleteById") {
|
||||||
test("이미 예약 중인 테마라면 409 예외를 던진다.") {
|
val themeId = 1L
|
||||||
val themeId = 1L
|
val theme: ThemeEntity = ThemeFixture.create(id = themeId)
|
||||||
|
|
||||||
every {
|
test("정상 응답") {
|
||||||
themeRepository.isReservedTheme(themeId)
|
every { themeFinder.findById(themeId) } returns theme
|
||||||
} returns true
|
every { themeWriter.delete(theme) } just Runs
|
||||||
|
|
||||||
val exception = shouldThrow<ThemeException> {
|
shouldNotThrow<ThemeException> {
|
||||||
themeService.deleteTheme(themeId)
|
themeService.deleteTheme(themeId)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exception.errorCode shouldBe ThemeErrorCode.THEME_ALREADY_RESERVED
|
test("테마를 찾을 수 없으면 예외 응답") {
|
||||||
|
every { themeFinder.findById(themeId) } throws ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
|
||||||
|
|
||||||
|
shouldThrow<ThemeException> {
|
||||||
|
themeService.deleteTheme(themeId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ThemeErrorCode.THEME_NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("예약이 있는 테마이면 예외 응답") {
|
||||||
|
every { themeFinder.findById(themeId) } returns theme
|
||||||
|
every { themeWriter.delete(theme) } throws ThemeException(ThemeErrorCode.THEME_ALREADY_RESERVED)
|
||||||
|
|
||||||
|
shouldThrow<ThemeException> {
|
||||||
|
themeService.deleteTheme(themeId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ThemeErrorCode.THEME_ALREADY_RESERVED
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -0,0 +1,76 @@
|
|||||||
|
package roomescape.theme.implement
|
||||||
|
|
||||||
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.collections.shouldHaveSize
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
|
import roomescape.theme.exception.ThemeErrorCode
|
||||||
|
import roomescape.theme.exception.ThemeException
|
||||||
|
import roomescape.theme.infrastructure.persistence.ThemeRepository
|
||||||
|
import roomescape.util.ThemeFixture
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
class ThemeFinderTest : FunSpec({
|
||||||
|
val themeRepository: ThemeRepository = mockk()
|
||||||
|
|
||||||
|
val themeFinder = ThemeFinder(themeRepository)
|
||||||
|
|
||||||
|
context("findAll") {
|
||||||
|
test("모든 테마를 조회한다.") {
|
||||||
|
every {
|
||||||
|
themeRepository.findAll()
|
||||||
|
} returns listOf(mockk(), mockk(), mockk())
|
||||||
|
|
||||||
|
themeRepository.findAll() shouldHaveSize 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("findById") {
|
||||||
|
val timeId = 1L
|
||||||
|
test("동일한 ID인 테마를 찾아 응답한다.") {
|
||||||
|
every {
|
||||||
|
themeRepository.findByIdOrNull(timeId)
|
||||||
|
} returns mockk()
|
||||||
|
|
||||||
|
themeFinder.findById(timeId)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
themeRepository.findByIdOrNull(timeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("동일한 ID인 테마가 없으면 실패한다.") {
|
||||||
|
every {
|
||||||
|
themeRepository.findByIdOrNull(timeId)
|
||||||
|
} returns null
|
||||||
|
|
||||||
|
shouldThrow<ThemeException> {
|
||||||
|
themeFinder.findById(timeId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ThemeErrorCode.THEME_NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("findMostReservedThemes") {
|
||||||
|
test("입력된 개수보다 조회된 개수가 작으면 조회된 개수만큼 반환한다.") {
|
||||||
|
val count = 10
|
||||||
|
val startFrom = LocalDate.now().minusDays(7)
|
||||||
|
val endAt = LocalDate.now().minusDays(1)
|
||||||
|
|
||||||
|
every {
|
||||||
|
themeRepository.findPopularThemes(startFrom, endAt, count)
|
||||||
|
} returns listOf(
|
||||||
|
ThemeFixture.create(id = 1, name = "name1"),
|
||||||
|
ThemeFixture.create(id = 2, name = "name2"),
|
||||||
|
ThemeFixture.create(id = 3, name = "name3"),
|
||||||
|
)
|
||||||
|
|
||||||
|
themeFinder.findMostReservedThemes(count, startFrom, endAt) shouldHaveSize 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
package roomescape.theme.implement
|
||||||
|
|
||||||
|
import io.kotest.assertions.throwables.shouldNotThrow
|
||||||
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import roomescape.theme.exception.ThemeErrorCode
|
||||||
|
import roomescape.theme.exception.ThemeException
|
||||||
|
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||||
|
import roomescape.theme.infrastructure.persistence.ThemeRepository
|
||||||
|
import roomescape.util.ThemeFixture
|
||||||
|
|
||||||
|
class ThemeValidatorTest : FunSpec({
|
||||||
|
val themeRepository: ThemeRepository = mockk()
|
||||||
|
|
||||||
|
val themeValidator = ThemeValidator(themeRepository)
|
||||||
|
|
||||||
|
context("validateNameAlreadyExists") {
|
||||||
|
val name = "name"
|
||||||
|
|
||||||
|
test("같은 이름을 가진 테마가 있으면 예외를 던진다.") {
|
||||||
|
every {
|
||||||
|
themeRepository.existsByName(name)
|
||||||
|
} returns true
|
||||||
|
|
||||||
|
shouldThrow<ThemeException> {
|
||||||
|
themeValidator.validateNameAlreadyExists(name)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ThemeErrorCode.THEME_NAME_DUPLICATED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("같은 이름을 가진 테마가 없으면 종료한다.") {
|
||||||
|
every {
|
||||||
|
themeRepository.existsByName(name)
|
||||||
|
} returns false
|
||||||
|
|
||||||
|
shouldNotThrow<ThemeException> {
|
||||||
|
themeValidator.validateNameAlreadyExists(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("validateIsReserved") {
|
||||||
|
test("입력된 id가 null 이면 예외를 던진다.") {
|
||||||
|
shouldThrow<ThemeException> {
|
||||||
|
themeValidator.validateIsReserved(ThemeFixture.create(id = null))
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ThemeErrorCode.INVALID_REQUEST_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val theme: ThemeEntity = ThemeFixture.create(id = 1L, name = "name")
|
||||||
|
|
||||||
|
test("예약이 있는 테마이면 예외를 던진다.") {
|
||||||
|
every {
|
||||||
|
themeRepository.isReservedTheme(theme.id!!)
|
||||||
|
} returns true
|
||||||
|
|
||||||
|
shouldThrow<ThemeException> {
|
||||||
|
themeValidator.validateIsReserved(theme)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ThemeErrorCode.THEME_ALREADY_RESERVED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("예약이 없는 테마이면 종료한다.") {
|
||||||
|
every {
|
||||||
|
themeRepository.isReservedTheme(theme.id!!)
|
||||||
|
} returns false
|
||||||
|
|
||||||
|
shouldNotThrow<ThemeException> {
|
||||||
|
themeValidator.validateIsReserved(theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
package roomescape.theme.implement
|
||||||
|
|
||||||
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.*
|
||||||
|
import roomescape.common.config.next
|
||||||
|
import roomescape.theme.exception.ThemeErrorCode
|
||||||
|
import roomescape.theme.exception.ThemeException
|
||||||
|
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||||
|
import roomescape.theme.infrastructure.persistence.ThemeRepository
|
||||||
|
import roomescape.util.ThemeFixture
|
||||||
|
import roomescape.util.TsidFactory
|
||||||
|
|
||||||
|
class ThemeWriterTest : FunSpec({
|
||||||
|
|
||||||
|
val themeValidator: ThemeValidator = mockk()
|
||||||
|
val themeRepository: ThemeRepository = mockk()
|
||||||
|
|
||||||
|
val themeWriter = ThemeWriter(themeValidator, themeRepository, TsidFactory)
|
||||||
|
|
||||||
|
context("create") {
|
||||||
|
val name = "name"
|
||||||
|
val description = "description"
|
||||||
|
val thumbnail = "thumbnail"
|
||||||
|
|
||||||
|
test("중복된 이름이 있으면 실패한다.") {
|
||||||
|
every {
|
||||||
|
themeValidator.validateNameAlreadyExists(name)
|
||||||
|
} throws ThemeException(ThemeErrorCode.THEME_NAME_DUPLICATED)
|
||||||
|
|
||||||
|
shouldThrow<ThemeException> {
|
||||||
|
themeWriter.create(name, description, thumbnail)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ThemeErrorCode.THEME_NAME_DUPLICATED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("중복된 이름이 없으면 저장한다.") {
|
||||||
|
every {
|
||||||
|
themeValidator.validateNameAlreadyExists(name)
|
||||||
|
} just Runs
|
||||||
|
|
||||||
|
every {
|
||||||
|
themeRepository.save(any())
|
||||||
|
} returns ThemeFixture.create(name = name, description = description, thumbnail = thumbnail)
|
||||||
|
|
||||||
|
themeWriter.create(name, description, thumbnail)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
themeRepository.save(any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("delete") {
|
||||||
|
val theme: ThemeEntity = ThemeFixture.create(id = TsidFactory.next())
|
||||||
|
test("예약이 있는 테마이면 실패한다.") {
|
||||||
|
every {
|
||||||
|
themeValidator.validateIsReserved(theme)
|
||||||
|
} throws ThemeException(ThemeErrorCode.THEME_ALREADY_RESERVED)
|
||||||
|
|
||||||
|
shouldThrow<ThemeException> {
|
||||||
|
themeWriter.delete(theme)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ThemeErrorCode.THEME_ALREADY_RESERVED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("예약이 없는 테마이면 제거한다.") {
|
||||||
|
every {
|
||||||
|
themeValidator.validateIsReserved(theme)
|
||||||
|
} just Runs
|
||||||
|
|
||||||
|
every {
|
||||||
|
themeRepository.delete(theme)
|
||||||
|
} just Runs
|
||||||
|
|
||||||
|
themeWriter.delete(theme)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
themeRepository.delete(theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -5,7 +5,7 @@ import io.kotest.matchers.collections.shouldContainInOrder
|
|||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
import jakarta.persistence.EntityManager
|
import jakarta.persistence.EntityManager
|
||||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||||
import roomescape.theme.util.TestThemeCreateUtil
|
import roomescape.theme.util.TestThemeDataHelper
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|
||||||
@DataJpaTest(showSql = false)
|
@DataJpaTest(showSql = false)
|
||||||
@ -14,12 +14,13 @@ class ThemeRepositoryTest(
|
|||||||
val entityManager: EntityManager
|
val entityManager: EntityManager
|
||||||
) : FunSpec() {
|
) : FunSpec() {
|
||||||
|
|
||||||
|
val helper = TestThemeDataHelper(entityManager, transactionTemplate = null)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
context("findTopNThemeBetweenStartDateAndEndDate") {
|
context("findTopNThemeBetweenStartDateAndEndDate") {
|
||||||
beforeTest {
|
beforeTest {
|
||||||
for (i in 1..10) {
|
for (i in 1..10) {
|
||||||
TestThemeCreateUtil.createThemeWithReservations(
|
helper.createThemeWithReservations(
|
||||||
entityManager = entityManager,
|
|
||||||
name = "테마$i",
|
name = "테마$i",
|
||||||
reservedCount = i,
|
reservedCount = i,
|
||||||
date = LocalDate.now().minusDays(i.toLong()),
|
date = LocalDate.now().minusDays(i.toLong()),
|
||||||
@ -27,7 +28,7 @@ class ThemeRepositoryTest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("지난 10일간 예약 수가 가장 많은 테마 5개를 조회한다.") {
|
test("지난 10일간 예약 수가 가장 많은 테마 5개를 조회") {
|
||||||
themeRepository.findPopularThemes(
|
themeRepository.findPopularThemes(
|
||||||
LocalDate.now().minusDays(10),
|
LocalDate.now().minusDays(10),
|
||||||
LocalDate.now().minusDays(1),
|
LocalDate.now().minusDays(1),
|
||||||
@ -40,7 +41,7 @@ class ThemeRepositoryTest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("8일 전부터 5일 전까지 예약 수가 가장 많은 테마 3개를 조회한다.") {
|
test("8일 전부터 5일 전까지 예약 수가 가장 많은 테마 3개를 조회") {
|
||||||
themeRepository.findPopularThemes(
|
themeRepository.findPopularThemes(
|
||||||
LocalDate.now().minusDays(8),
|
LocalDate.now().minusDays(8),
|
||||||
LocalDate.now().minusDays(5),
|
LocalDate.now().minusDays(5),
|
||||||
@ -53,9 +54,8 @@ class ThemeRepositoryTest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("예약 수가 동일하면 먼저 생성된 테마를 우선 조회한다.") {
|
test("예약 수가 동일하면 먼저 생성된 테마를 우선 조회") {
|
||||||
TestThemeCreateUtil.createThemeWithReservations(
|
helper.createThemeWithReservations(
|
||||||
entityManager = entityManager,
|
|
||||||
name = "테마11",
|
name = "테마11",
|
||||||
reservedCount = 5,
|
reservedCount = 5,
|
||||||
date = LocalDate.now().minusDays(5),
|
date = LocalDate.now().minusDays(5),
|
||||||
@ -73,7 +73,7 @@ class ThemeRepositoryTest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("입력된 갯수보다 조회된 갯수가 작으면, 조회된 갯수만큼 반환한다.") {
|
test("입력된 갯수보다 조회된 갯수가 작으면, 조회된 갯수만큼 반환") {
|
||||||
themeRepository.findPopularThemes(
|
themeRepository.findPopularThemes(
|
||||||
LocalDate.now().minusDays(10),
|
LocalDate.now().minusDays(10),
|
||||||
LocalDate.now().minusDays(6),
|
LocalDate.now().minusDays(6),
|
||||||
@ -83,7 +83,7 @@ class ThemeRepositoryTest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("입력된 갯수보다 조회된 갯수가 많으면, 입력된 갯수만큼 반환한다.") {
|
test("입력된 갯수보다 조회된 갯수가 많으면, 입력된 갯수만큼 반환") {
|
||||||
themeRepository.findPopularThemes(
|
themeRepository.findPopularThemes(
|
||||||
LocalDate.now().minusDays(10),
|
LocalDate.now().minusDays(10),
|
||||||
LocalDate.now().minusDays(1),
|
LocalDate.now().minusDays(1),
|
||||||
@ -93,7 +93,7 @@ class ThemeRepositoryTest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("입력된 날짜 범위에 예약된 테마가 없을 경우 빈 리스트를 반환한다.") {
|
test("입력된 날짜 범위에 예약된 테마가 없을 경우 빈 리스트 반환") {
|
||||||
themeRepository.findPopularThemes(
|
themeRepository.findPopularThemes(
|
||||||
LocalDate.now().plusDays(1),
|
LocalDate.now().plusDays(1),
|
||||||
LocalDate.now().plusDays(10),
|
LocalDate.now().plusDays(10),
|
||||||
@ -106,26 +106,24 @@ class ThemeRepositoryTest(
|
|||||||
context("existsByName ") {
|
context("existsByName ") {
|
||||||
val themeName = "test-theme"
|
val themeName = "test-theme"
|
||||||
beforeTest {
|
beforeTest {
|
||||||
TestThemeCreateUtil.createThemeWithReservations(
|
helper.createThemeWithReservations(
|
||||||
entityManager = entityManager,
|
|
||||||
name = themeName,
|
name = themeName,
|
||||||
reservedCount = 0,
|
reservedCount = 0,
|
||||||
date = LocalDate.now()
|
date = LocalDate.now()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
test("테마 이름이 존재하면 true를 반환한다.") {
|
test("테마 이름이 존재하면 true 반환") {
|
||||||
themeRepository.existsByName(themeName) shouldBe true
|
themeRepository.existsByName(themeName) shouldBe true
|
||||||
}
|
}
|
||||||
|
|
||||||
test("테마 이름이 존재하지 않으면 false를 반환한다.") {
|
test("테마 이름이 존재하지 않으면 false 반환") {
|
||||||
themeRepository.existsByName(themeName.repeat(2)) shouldBe false
|
themeRepository.existsByName(themeName.repeat(2)) shouldBe false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context("isReservedTheme") {
|
context("isReservedTheme") {
|
||||||
test("테마가 예약 중이면 true를 반환한다.") {
|
test("테마가 예약 중이면 true 반환") {
|
||||||
val theme = TestThemeCreateUtil.createThemeWithReservations(
|
val theme = helper.createThemeWithReservations(
|
||||||
entityManager = entityManager,
|
|
||||||
name = "예약된 테마",
|
name = "예약된 테마",
|
||||||
reservedCount = 1,
|
reservedCount = 1,
|
||||||
date = LocalDate.now()
|
date = LocalDate.now()
|
||||||
@ -133,9 +131,8 @@ class ThemeRepositoryTest(
|
|||||||
themeRepository.isReservedTheme(theme.id!!) shouldBe true
|
themeRepository.isReservedTheme(theme.id!!) shouldBe true
|
||||||
}
|
}
|
||||||
|
|
||||||
test("테마가 예약 중이 아니면 false를 반환한다.") {
|
test("테마가 예약 중이 아니면 false 반환") {
|
||||||
val theme = TestThemeCreateUtil.createThemeWithReservations(
|
val theme = helper.createThemeWithReservations(
|
||||||
entityManager = entityManager,
|
|
||||||
name = "예약되지 않은 테마",
|
name = "예약되지 않은 테마",
|
||||||
reservedCount = 0,
|
reservedCount = 0,
|
||||||
date = LocalDate.now()
|
date = LocalDate.now()
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package roomescape.theme.util
|
package roomescape.theme.util
|
||||||
|
|
||||||
import jakarta.persistence.EntityManager
|
import jakarta.persistence.EntityManager
|
||||||
|
import org.springframework.transaction.support.TransactionTemplate
|
||||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||||
@ -12,13 +13,25 @@ import roomescape.util.TimeFixture
|
|||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
|
|
||||||
object TestThemeCreateUtil {
|
class TestThemeDataHelper(
|
||||||
fun createThemeWithReservations(
|
val entityManager: EntityManager,
|
||||||
entityManager: EntityManager,
|
val transactionTemplate: TransactionTemplate?
|
||||||
name: String,
|
) {
|
||||||
reservedCount: Int,
|
/**
|
||||||
date: LocalDate,
|
* GET /themes/most-reserved-last-week API와 관련 Repository 테스트에 사용
|
||||||
): ThemeEntity {
|
* @param name: 테마 이름
|
||||||
|
* @param reservedCount: 이 테마가 예약된 횟수
|
||||||
|
* @param date: reservedCount 개의 예약을 만들 때 사용할 날짜
|
||||||
|
*/
|
||||||
|
fun createThemeWithReservations(name: String, reservedCount: Int, date: LocalDate): ThemeEntity =
|
||||||
|
if (transactionTemplate == null) {
|
||||||
|
createAndGet(name, reservedCount, date)
|
||||||
|
} else {
|
||||||
|
transactionTemplate.execute { createAndGet(name, reservedCount, date) }!!
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun createAndGet(name: String, reservedCount: Int, date: LocalDate): ThemeEntity {
|
||||||
val themeEntity: ThemeEntity = ThemeFixture.create(name = name).also { entityManager.persist(it) }
|
val themeEntity: ThemeEntity = ThemeFixture.create(name = name).also { entityManager.persist(it) }
|
||||||
val member: MemberEntity = MemberFixture.create().also { entityManager.persist(it) }
|
val member: MemberEntity = MemberFixture.create().also { entityManager.persist(it) }
|
||||||
|
|
||||||
@ -32,7 +45,7 @@ object TestThemeCreateUtil {
|
|||||||
theme = themeEntity,
|
theme = themeEntity,
|
||||||
member = member,
|
member = member,
|
||||||
time = time,
|
time = time,
|
||||||
status = ReservationStatus.CONFIRMED
|
status = ReservationStatus.entries.random()
|
||||||
).also { entityManager.persist(it) }
|
).also { entityManager.persist(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ import org.hamcrest.Matchers.equalTo
|
|||||||
import org.springframework.boot.test.context.SpringBootTest
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
import org.springframework.boot.test.web.server.LocalServerPort
|
import org.springframework.boot.test.web.server.LocalServerPort
|
||||||
import org.springframework.transaction.support.TransactionTemplate
|
import org.springframework.transaction.support.TransactionTemplate
|
||||||
import roomescape.theme.util.TestThemeCreateUtil
|
import roomescape.theme.util.TestThemeDataHelper
|
||||||
import roomescape.util.CleanerMode
|
import roomescape.util.CleanerMode
|
||||||
import roomescape.util.DatabaseCleanerExtension
|
import roomescape.util.DatabaseCleanerExtension
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
@ -20,16 +20,16 @@ class MostReservedThemeApiTest(
|
|||||||
@LocalServerPort val port: Int,
|
@LocalServerPort val port: Int,
|
||||||
val transactionTemplate: TransactionTemplate,
|
val transactionTemplate: TransactionTemplate,
|
||||||
val entityManager: EntityManager,
|
val entityManager: EntityManager,
|
||||||
) : FunSpec({
|
) : FunSpec({ extension(DatabaseCleanerExtension(mode = CleanerMode.AFTER_SPEC)) }) {
|
||||||
extension(DatabaseCleanerExtension(mode = CleanerMode.AFTER_SPEC))
|
|
||||||
}) {
|
val helper = TestThemeDataHelper(entityManager, transactionTemplate)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
beforeSpec {
|
beforeSpec {
|
||||||
transactionTemplate.executeWithoutResult {
|
transactionTemplate.executeWithoutResult {
|
||||||
// 지난 7일간 예약된 테마 10개 생성
|
// 7일 전 ~ 1일 전 예약된 테마 10개 생성
|
||||||
for (i in 1..10) {
|
for (i in 1..10) {
|
||||||
TestThemeCreateUtil.createThemeWithReservations(
|
helper.createThemeWithReservations(
|
||||||
entityManager = entityManager,
|
|
||||||
name = "테마$i",
|
name = "테마$i",
|
||||||
reservedCount = 1,
|
reservedCount = 1,
|
||||||
date = LocalDate.now().minusDays(Random.nextLong(1, 7))
|
date = LocalDate.now().minusDays(Random.nextLong(1, 7))
|
||||||
@ -37,16 +37,22 @@ class MostReservedThemeApiTest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 8일 전 예약된 테마 1개 생성
|
// 8일 전 예약된 테마 1개 생성
|
||||||
TestThemeCreateUtil.createThemeWithReservations(
|
helper.createThemeWithReservations(
|
||||||
entityManager = entityManager,
|
|
||||||
name = "테마11",
|
name = "테마11",
|
||||||
reservedCount = 1,
|
reservedCount = 1,
|
||||||
date = LocalDate.now().minusDays(8)
|
date = LocalDate.now().minusDays(8)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 당일 예약된 테마 1개 생성
|
||||||
|
helper.createThemeWithReservations(
|
||||||
|
name = "테마12",
|
||||||
|
reservedCount = 1,
|
||||||
|
date = LocalDate.now()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context("지난 주 가장 많이 예약된 테마 API") {
|
context("GET /themes/most-reserved-last-week") {
|
||||||
val endpoint = "/themes/most-reserved-last-week"
|
val endpoint = "/themes/most-reserved-last-week"
|
||||||
|
|
||||||
test("count 파라미터가 없으면 10개를 반환한다") {
|
test("count 파라미터가 없으면 10개를 반환한다") {
|
||||||
@ -87,8 +93,8 @@ class MostReservedThemeApiTest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
test("지난 7일 동안의 예약만 집계한다") {
|
test("지난 7일 동안의 예약만 집계한다") {
|
||||||
// 8일 전에 예약된 테마는 집계에서 제외되어야 한다.
|
// beforeSpec 에서 정의한 테스트 데이터 중, 8일 전 / 당일 예약 테마는 제외되어야 한다.
|
||||||
val count = 11
|
val count = 12
|
||||||
Given {
|
Given {
|
||||||
port(port)
|
port(port)
|
||||||
param("count", count)
|
param("count", count)
|
||||||
|
|||||||
@ -1,42 +1,32 @@
|
|||||||
package roomescape.theme.web
|
package roomescape.theme.web
|
||||||
|
|
||||||
import com.ninjasquad.springmockk.MockkBean
|
import com.ninjasquad.springmockk.MockkBean
|
||||||
import com.ninjasquad.springmockk.SpykBean
|
|
||||||
import io.kotest.assertions.assertSoftly
|
|
||||||
import io.kotest.matchers.collections.shouldContainAll
|
|
||||||
import io.kotest.matchers.shouldBe
|
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
|
||||||
import io.mockk.runs
|
|
||||||
import org.hamcrest.Matchers.equalTo
|
import org.hamcrest.Matchers.equalTo
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||||
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext
|
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import roomescape.auth.exception.AuthErrorCode
|
import roomescape.auth.exception.AuthErrorCode
|
||||||
|
import roomescape.common.exception.CommonErrorCode
|
||||||
import roomescape.theme.business.ThemeService
|
import roomescape.theme.business.ThemeService
|
||||||
import roomescape.theme.exception.ThemeErrorCode
|
import roomescape.theme.exception.ThemeErrorCode
|
||||||
import roomescape.theme.infrastructure.persistence.ThemeRepository
|
import roomescape.theme.exception.ThemeException
|
||||||
import roomescape.util.RoomescapeApiTest
|
import roomescape.util.RoomescapeApiTest
|
||||||
import roomescape.util.ThemeFixture
|
import roomescape.util.ThemeFixture
|
||||||
|
|
||||||
@WebMvcTest(ThemeController::class)
|
@WebMvcTest(ThemeController::class)
|
||||||
class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
|
class ThemeControllerTest(val mockMvc: MockMvc) : RoomescapeApiTest() {
|
||||||
|
@MockkBean
|
||||||
@SpykBean
|
|
||||||
private lateinit var themeService: ThemeService
|
private lateinit var themeService: ThemeService
|
||||||
|
|
||||||
@MockkBean
|
|
||||||
private lateinit var themeRepository: ThemeRepository
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Given("모든 테마를 조회할 때") {
|
Given("GET /themes 요청을") {
|
||||||
val endpoint = "/themes"
|
val endpoint = "/themes"
|
||||||
|
|
||||||
When("로그인 상태가 아니라면") {
|
When("로그인 하지 않은 사용자가 보내면") {
|
||||||
doNotLogin()
|
doNotLogin()
|
||||||
|
|
||||||
Then("에러 응답을 받는다.") {
|
Then("예외 응답") {
|
||||||
val expectedError = AuthErrorCode.MEMBER_NOT_FOUND
|
val expectedError = AuthErrorCode.MEMBER_NOT_FOUND
|
||||||
runGetTest(
|
runGetTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
@ -52,34 +42,32 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
|
|||||||
When("로그인 상태라면") {
|
When("로그인 상태라면") {
|
||||||
loginAsUser()
|
loginAsUser()
|
||||||
|
|
||||||
Then("조회에 성공한다.") {
|
Then("정상 응답") {
|
||||||
every {
|
every {
|
||||||
themeRepository.findAll()
|
themeService.findThemes()
|
||||||
} returns listOf(
|
} returns listOf(
|
||||||
ThemeFixture.create(id = 1, name = "theme1"),
|
ThemeFixture.create(id = 1, name = "theme1"),
|
||||||
ThemeFixture.create(id = 2, name = "theme2"),
|
ThemeFixture.create(id = 2, name = "theme2"),
|
||||||
ThemeFixture.create(id = 3, name = "theme3")
|
ThemeFixture.create(id = 3, name = "theme3")
|
||||||
)
|
).toRetrieveListResponse()
|
||||||
|
|
||||||
val response: ThemeRetrieveListResponse = runGetTest(
|
runGetTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
endpoint = endpoint,
|
endpoint = endpoint,
|
||||||
) {
|
) {
|
||||||
status { isOk() }
|
status { isOk() }
|
||||||
content {
|
content {
|
||||||
contentType(MediaType.APPLICATION_JSON)
|
contentType(MediaType.APPLICATION_JSON)
|
||||||
|
jsonPath("$.data.themes[0].id") { value(1) }
|
||||||
|
jsonPath("$.data.themes[1].id") { value(2) }
|
||||||
|
jsonPath("$.data.themes[2].id") { value(3) }
|
||||||
}
|
}
|
||||||
}.andReturn().readValue(ThemeRetrieveListResponse::class.java)
|
|
||||||
|
|
||||||
assertSoftly(response.themes) {
|
|
||||||
it.size shouldBe 3
|
|
||||||
it.map { m -> m.name } shouldContainAll listOf("theme1", "theme2", "theme3")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Given("테마를 추가할 때") {
|
Given("POST /themes 요청을") {
|
||||||
val endpoint = "/themes"
|
val endpoint = "/themes"
|
||||||
val request = ThemeCreateRequest(
|
val request = ThemeCreateRequest(
|
||||||
name = "theme1",
|
name = "theme1",
|
||||||
@ -87,9 +75,10 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
|
|||||||
thumbnail = "http://example.com/thumbnail1.jpg"
|
thumbnail = "http://example.com/thumbnail1.jpg"
|
||||||
)
|
)
|
||||||
|
|
||||||
When("로그인 상태가 아니라면") {
|
When("로그인 하지 않은 사용자가 보내면") {
|
||||||
doNotLogin()
|
doNotLogin()
|
||||||
Then("에러 응답을 받는다.") {
|
|
||||||
|
Then("예외 응답") {
|
||||||
val expectedError = AuthErrorCode.MEMBER_NOT_FOUND
|
val expectedError = AuthErrorCode.MEMBER_NOT_FOUND
|
||||||
runPostTest(
|
runPostTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
@ -102,9 +91,10 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
When("관리자가 아닌 회원은") {
|
When("관리자가 아닌 사용자가 보내면") {
|
||||||
loginAsUser()
|
loginAsUser()
|
||||||
Then("에러 응답을 받는다.") {
|
|
||||||
|
Then("예외 응답") {
|
||||||
val expectedError = AuthErrorCode.ACCESS_DENIED
|
val expectedError = AuthErrorCode.ACCESS_DENIED
|
||||||
runPostTest(
|
runPostTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
@ -117,15 +107,17 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
When("동일한 이름의 테마가 있으면") {
|
When("관리자가 보낼 때") {
|
||||||
loginAsAdmin()
|
beforeTest {
|
||||||
|
loginAsAdmin()
|
||||||
|
}
|
||||||
|
|
||||||
val expectedError = ThemeErrorCode.THEME_NAME_DUPLICATED
|
Then("동일한 이름의 테마가 있으면 예외 응답") {
|
||||||
|
val expectedError = ThemeErrorCode.THEME_NAME_DUPLICATED
|
||||||
|
|
||||||
Then("에러 응답을 받는다.") {
|
|
||||||
every {
|
every {
|
||||||
themeRepository.existsByName(request.name)
|
themeService.createTheme(request)
|
||||||
} returns true
|
} throws ThemeException(expectedError)
|
||||||
|
|
||||||
runPostTest(
|
runPostTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
@ -136,80 +128,70 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
|
|||||||
jsonPath("$.code") { value(expectedError.errorCode) }
|
jsonPath("$.code") { value(expectedError.errorCode) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
When("값이 잘못 입력되면 400 에러를 응답한다") {
|
When("입력 값의 형식이 잘못되면 예외 응답") {
|
||||||
beforeTest {
|
val request = ThemeCreateRequest(
|
||||||
loginAsAdmin()
|
name = "theme1",
|
||||||
}
|
description = "description1",
|
||||||
|
thumbnail = "http://example.com/thumbnail1.jpg"
|
||||||
|
)
|
||||||
|
|
||||||
val request = ThemeCreateRequest(
|
fun runTest(request: ThemeCreateRequest) {
|
||||||
name = "theme1",
|
val expectedError = CommonErrorCode.INVALID_INPUT_VALUE
|
||||||
description = "description1",
|
|
||||||
thumbnail = "http://example.com/thumbnail1.jpg"
|
|
||||||
)
|
|
||||||
|
|
||||||
fun runTest(request: ThemeCreateRequest) {
|
runPostTest(
|
||||||
runPostTest(
|
mockMvc = mockMvc,
|
||||||
mockMvc = mockMvc,
|
endpoint = endpoint,
|
||||||
endpoint = endpoint,
|
body = request,
|
||||||
body = request,
|
) {
|
||||||
) {
|
status { isEqualTo(expectedError.httpStatus.value()) }
|
||||||
status { isBadRequest() }
|
jsonPath("$.code") { value(expectedError.errorCode) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Then("이름이 공백인 경우") {
|
||||||
|
val invalidRequest = request.copy(name = " ")
|
||||||
|
runTest(invalidRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
Then("이름이 20글자를 초과하는 경우") {
|
||||||
|
val invalidRequest = request.copy(name = "a".repeat(21))
|
||||||
|
runTest(invalidRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
Then("설명이 공백인 경우") {
|
||||||
|
val invalidRequest = request.copy(description = " ")
|
||||||
|
runTest(invalidRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
Then("설명이 100글자를 초과하는 경우") {
|
||||||
|
val invalidRequest = request.copy(description = "a".repeat(101))
|
||||||
|
runTest(invalidRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
Then("썸네일이 공백인 경우") {
|
||||||
|
val invalidRequest = request.copy(thumbnail = " ")
|
||||||
|
runTest(invalidRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
Then("썸네일이 URL 형식이 아닌 경우") {
|
||||||
|
val invalidRequest = request.copy(thumbnail = "invalid-url")
|
||||||
|
runTest(invalidRequest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Then("이름이 공백인 경우") {
|
Then("정상 응답") {
|
||||||
val invalidRequest = request.copy(name = " ")
|
val theme = ThemeFixture.create(
|
||||||
runTest(invalidRequest)
|
id = 1,
|
||||||
}
|
name = request.name,
|
||||||
|
description = request.description,
|
||||||
|
thumbnail = request.thumbnail
|
||||||
|
)
|
||||||
|
|
||||||
Then("이름이 20글자를 초과하는 경우") {
|
every {
|
||||||
val invalidRequest = request.copy(name = "a".repeat(21))
|
themeService.createTheme(request)
|
||||||
runTest(invalidRequest)
|
} returns theme.toCreateResponse()
|
||||||
}
|
|
||||||
|
|
||||||
Then("설명이 공백인 경우") {
|
|
||||||
val invalidRequest = request.copy(description = " ")
|
|
||||||
runTest(invalidRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
Then("설명이 100글자를 초과하는 경우") {
|
|
||||||
val invalidRequest = request.copy(description = "a".repeat(101))
|
|
||||||
runTest(invalidRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
Then("썸네일이 공백인 경우") {
|
|
||||||
val invalidRequest = request.copy(thumbnail = " ")
|
|
||||||
runTest(invalidRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
Then("썸네일이 URL 형식이 아닌 경우") {
|
|
||||||
val invalidRequest = request.copy(thumbnail = "invalid-url")
|
|
||||||
runTest(invalidRequest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
When("저장에 성공하면") {
|
|
||||||
loginAsAdmin()
|
|
||||||
|
|
||||||
val theme = ThemeFixture.create(
|
|
||||||
id = 1,
|
|
||||||
name = request.name,
|
|
||||||
description = request.description,
|
|
||||||
thumbnail = request.thumbnail
|
|
||||||
)
|
|
||||||
|
|
||||||
every {
|
|
||||||
themeService.createTheme(request)
|
|
||||||
} returns ThemeRetrieveResponse(
|
|
||||||
id = theme.id!!,
|
|
||||||
name = theme.name,
|
|
||||||
description = theme.description,
|
|
||||||
thumbnail = theme.thumbnail
|
|
||||||
)
|
|
||||||
|
|
||||||
Then("201 응답을 받는다.") {
|
|
||||||
runPostTest(
|
runPostTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
endpoint = endpoint,
|
endpoint = endpoint,
|
||||||
@ -228,13 +210,14 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Given("테마를 제거할 때") {
|
Given("DELETE /themes/{id} 요청을") {
|
||||||
val themeId = 1L
|
val themeId = 1L
|
||||||
val endpoint = "/themes/$themeId"
|
val endpoint = "/themes/$themeId"
|
||||||
|
|
||||||
When("로그인 상태가 아니라면") {
|
When("관리자가 아닌 사용자가 보내면 예외 응답") {
|
||||||
doNotLogin()
|
Then("로그인 하지 않은 경우") {
|
||||||
Then("에러 응답을 받는다.") {
|
doNotLogin()
|
||||||
|
|
||||||
val expectedError = AuthErrorCode.MEMBER_NOT_FOUND
|
val expectedError = AuthErrorCode.MEMBER_NOT_FOUND
|
||||||
runDeleteTest(
|
runDeleteTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
@ -244,11 +227,10 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
|
|||||||
jsonPath("$.code", equalTo(expectedError.errorCode))
|
jsonPath("$.code", equalTo(expectedError.errorCode))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
When("관리자가 아닌 회원은") {
|
Then("로그인은 하였으나 관리자가 아닌 경우") {
|
||||||
loginAsUser()
|
loginAsUser()
|
||||||
Then("에러 응답을 받는다.") {
|
|
||||||
val expectedError = AuthErrorCode.ACCESS_DENIED
|
val expectedError = AuthErrorCode.ACCESS_DENIED
|
||||||
runDeleteTest(
|
runDeleteTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
@ -260,14 +242,16 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
When("이미 예약된 테마이면") {
|
When("관리자가 보낼 때") {
|
||||||
loginAsAdmin()
|
beforeTest {
|
||||||
val expectedError = ThemeErrorCode.THEME_ALREADY_RESERVED
|
loginAsAdmin()
|
||||||
|
}
|
||||||
|
|
||||||
Then("에러 응답을 받는다.") {
|
Then("이미 예약된 테마이면 예외 응답") {
|
||||||
|
val expectedError = ThemeErrorCode.THEME_ALREADY_RESERVED
|
||||||
every {
|
every {
|
||||||
themeRepository.isReservedTheme(themeId)
|
themeService.deleteTheme(themeId)
|
||||||
} returns true
|
} throws ThemeException(expectedError)
|
||||||
|
|
||||||
runDeleteTest(
|
runDeleteTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
@ -277,20 +261,10 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
|
|||||||
jsonPath("$.code") { value(expectedError.errorCode) }
|
jsonPath("$.code") { value(expectedError.errorCode) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
When("정상적으로 제거되면") {
|
Then("정상 응답") {
|
||||||
loginAsAdmin()
|
every { themeService.deleteTheme(themeId) } returns Unit
|
||||||
|
|
||||||
every {
|
|
||||||
themeRepository.isReservedTheme(themeId)
|
|
||||||
} returns false
|
|
||||||
|
|
||||||
every {
|
|
||||||
themeRepository.deleteById(themeId)
|
|
||||||
} just runs
|
|
||||||
|
|
||||||
Then("204 응답을 받는다.") {
|
|
||||||
runDeleteTest(
|
runDeleteTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
endpoint = endpoint,
|
endpoint = endpoint,
|
||||||
|
|||||||
@ -1,38 +1,50 @@
|
|||||||
package roomescape.time.business
|
package roomescape.time.business
|
||||||
|
|
||||||
|
import io.kotest.assertions.assertSoftly
|
||||||
import io.kotest.assertions.throwables.shouldNotThrow
|
import io.kotest.assertions.throwables.shouldNotThrow
|
||||||
import io.kotest.assertions.throwables.shouldThrow
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.collections.shouldContainExactly
|
||||||
|
import io.kotest.matchers.collections.shouldHaveSize
|
||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
import io.mockk.Runs
|
import io.mockk.Runs
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import roomescape.time.business.domain.TimeWithAvailability
|
||||||
import roomescape.reservation.infrastructure.persistence.ReservationRepository
|
import roomescape.theme.exception.ThemeErrorCode
|
||||||
|
import roomescape.theme.exception.ThemeException
|
||||||
import roomescape.time.exception.TimeErrorCode
|
import roomescape.time.exception.TimeErrorCode
|
||||||
import roomescape.time.exception.TimeException
|
import roomescape.time.exception.TimeException
|
||||||
import roomescape.time.infrastructure.persistence.TimeRepository
|
import roomescape.time.implement.TimeFinder
|
||||||
|
import roomescape.time.implement.TimeWriter
|
||||||
|
import roomescape.time.infrastructure.persistence.TimeEntity
|
||||||
import roomescape.time.web.TimeCreateRequest
|
import roomescape.time.web.TimeCreateRequest
|
||||||
import roomescape.util.TsidFactory
|
|
||||||
import roomescape.util.TimeFixture
|
import roomescape.util.TimeFixture
|
||||||
|
import java.time.LocalDate
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
|
|
||||||
class TimeServiceTest : FunSpec({
|
class TimeServiceTest : FunSpec({
|
||||||
val timeRepository: TimeRepository = mockk()
|
val timeFinder: TimeFinder = mockk()
|
||||||
val reservationRepository: ReservationRepository = mockk()
|
val timeWriter: TimeWriter = mockk()
|
||||||
|
|
||||||
val timeService = TimeService(
|
val timeService = TimeService(timeFinder, timeWriter)
|
||||||
tsidFactory = TsidFactory,
|
|
||||||
timeRepository = timeRepository,
|
context("findById") {
|
||||||
reservationRepository = reservationRepository
|
val id = 1L
|
||||||
)
|
|
||||||
|
test("정상 응답") {
|
||||||
|
every {
|
||||||
|
timeFinder.findById(id)
|
||||||
|
} returns TimeFixture.create(id = id)
|
||||||
|
|
||||||
|
timeService.findById(id).id shouldBe id
|
||||||
|
}
|
||||||
|
|
||||||
context("findTimeById") {
|
|
||||||
test("시간을 찾을 수 없으면 예외 응답") {
|
test("시간을 찾을 수 없으면 예외 응답") {
|
||||||
val id = 1L
|
every {
|
||||||
|
timeFinder.findById(id)
|
||||||
every { timeRepository.findByIdOrNull(id) } returns null
|
} throws TimeException(TimeErrorCode.TIME_NOT_FOUND)
|
||||||
|
|
||||||
shouldThrow<TimeException> {
|
shouldThrow<TimeException> {
|
||||||
timeService.findById(id)
|
timeService.findById(id)
|
||||||
@ -42,22 +54,81 @@ class TimeServiceTest : FunSpec({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context("findTimes") {
|
||||||
|
test("정상 응답") {
|
||||||
|
val times: List<TimeEntity> = listOf(
|
||||||
|
TimeFixture.create(startAt = LocalTime.now()),
|
||||||
|
TimeFixture.create(startAt = LocalTime.now().plusMinutes(1)),
|
||||||
|
TimeFixture.create(startAt = LocalTime.now().plusMinutes(2))
|
||||||
|
)
|
||||||
|
|
||||||
|
every {
|
||||||
|
timeFinder.findAll()
|
||||||
|
} returns times
|
||||||
|
|
||||||
|
val response = timeService.findTimes()
|
||||||
|
|
||||||
|
assertSoftly(response.times) {
|
||||||
|
it shouldHaveSize times.size
|
||||||
|
it.map { time -> time.startAt } shouldContainExactly times.map { time -> time.startAt }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("findTimesWithAvailability") {
|
||||||
|
val date = LocalDate.now()
|
||||||
|
val themeId = 1L
|
||||||
|
|
||||||
|
test("정상 응답") {
|
||||||
|
val times: List<TimeWithAvailability> = listOf(
|
||||||
|
TimeWithAvailability(1, LocalTime.now(), date, themeId, true),
|
||||||
|
TimeWithAvailability(2, LocalTime.now().plusMinutes(1), date, themeId, false),
|
||||||
|
TimeWithAvailability(3, LocalTime.now().plusMinutes(2), date, themeId, true)
|
||||||
|
)
|
||||||
|
|
||||||
|
every {
|
||||||
|
timeFinder.findAllWithAvailabilityByDateAndThemeId(date, themeId)
|
||||||
|
} returns times
|
||||||
|
|
||||||
|
val response = timeService.findTimesWithAvailability(date, themeId)
|
||||||
|
|
||||||
|
assertSoftly(response.times) {
|
||||||
|
it shouldHaveSize times.size
|
||||||
|
it.map { time -> time.isAvailable } shouldContainExactly times.map { time -> time.isReservable }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("테마를 찾을 수 없으면 예외 응답") {
|
||||||
|
every {
|
||||||
|
timeFinder.findAllWithAvailabilityByDateAndThemeId(date, themeId)
|
||||||
|
} throws ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
|
||||||
|
|
||||||
|
shouldThrow<ThemeException> {
|
||||||
|
timeService.findTimesWithAvailability(date, themeId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ThemeErrorCode.THEME_NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
context("createTime") {
|
context("createTime") {
|
||||||
val request = TimeCreateRequest(startAt = LocalTime.of(10, 0))
|
val request = TimeCreateRequest(startAt = LocalTime.of(10, 0))
|
||||||
|
|
||||||
test("정상 저장") {
|
test("정상 응답") {
|
||||||
every { timeRepository.existsByStartAt(request.startAt) } returns false
|
val time: TimeEntity = TimeFixture.create(startAt = request.startAt)
|
||||||
every { timeRepository.save(any()) } returns TimeFixture.create(
|
|
||||||
id = 1L,
|
every {
|
||||||
startAt = request.startAt
|
timeWriter.create(request.startAt)
|
||||||
)
|
} returns time
|
||||||
|
|
||||||
val response = timeService.createTime(request)
|
val response = timeService.createTime(request)
|
||||||
response.id shouldBe 1L
|
response.id shouldBe time.id
|
||||||
}
|
}
|
||||||
|
|
||||||
test("중복된 시간이 있으면 예외 응답") {
|
test("중복된 시간이 있으면 예외 응답") {
|
||||||
every { timeRepository.existsByStartAt(request.startAt) } returns true
|
every {
|
||||||
|
timeWriter.create(request.startAt)
|
||||||
|
} throws TimeException(TimeErrorCode.TIME_DUPLICATED)
|
||||||
|
|
||||||
shouldThrow<TimeException> {
|
shouldThrow<TimeException> {
|
||||||
timeService.createTime(request)
|
timeService.createTime(request)
|
||||||
@ -67,14 +138,13 @@ class TimeServiceTest : FunSpec({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context("removeTimeById") {
|
context("deleteTime") {
|
||||||
test("정상 제거 및 응답") {
|
test("정상 응답") {
|
||||||
val id = 1L
|
val id = 1L
|
||||||
val time = TimeFixture.create(id = id)
|
val time = TimeFixture.create(id = id)
|
||||||
|
|
||||||
every { timeRepository.findByIdOrNull(id) } returns time
|
every { timeFinder.findById(id) } returns time
|
||||||
every { reservationRepository.findAllByTime(time) } returns emptyList()
|
every { timeWriter.delete(time) } just Runs
|
||||||
every { timeRepository.delete(time) } just Runs
|
|
||||||
|
|
||||||
shouldNotThrow<Exception> {
|
shouldNotThrow<Exception> {
|
||||||
timeService.deleteTime(id)
|
timeService.deleteTime(id)
|
||||||
@ -84,7 +154,7 @@ class TimeServiceTest : FunSpec({
|
|||||||
test("시간을 찾을 수 없으면 예외 응답") {
|
test("시간을 찾을 수 없으면 예외 응답") {
|
||||||
val id = 1L
|
val id = 1L
|
||||||
|
|
||||||
every { timeRepository.findByIdOrNull(id) } returns null
|
every { timeFinder.findById(id) } throws TimeException(TimeErrorCode.TIME_NOT_FOUND)
|
||||||
|
|
||||||
shouldThrow<TimeException> {
|
shouldThrow<TimeException> {
|
||||||
timeService.deleteTime(id)
|
timeService.deleteTime(id)
|
||||||
@ -97,9 +167,8 @@ class TimeServiceTest : FunSpec({
|
|||||||
val id = 1L
|
val id = 1L
|
||||||
val time = TimeFixture.create()
|
val time = TimeFixture.create()
|
||||||
|
|
||||||
every { timeRepository.findByIdOrNull(id) } returns time
|
every { timeFinder.findById(id) } returns time
|
||||||
|
every { timeWriter.delete(time) } throws TimeException(TimeErrorCode.TIME_ALREADY_RESERVED)
|
||||||
every { reservationRepository.findAllByTime(time) } returns listOf(mockk())
|
|
||||||
|
|
||||||
shouldThrow<TimeException> {
|
shouldThrow<TimeException> {
|
||||||
timeService.deleteTime(id)
|
timeService.deleteTime(id)
|
||||||
|
|||||||
109
src/test/kotlin/roomescape/time/implement/TimeFinderTest.kt
Normal file
109
src/test/kotlin/roomescape/time/implement/TimeFinderTest.kt
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package roomescape.time.implement
|
||||||
|
|
||||||
|
import io.kotest.assertions.assertSoftly
|
||||||
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.collections.shouldHaveSize
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
|
import roomescape.reservation.implement.ReservationFinder
|
||||||
|
import roomescape.time.business.domain.TimeWithAvailability
|
||||||
|
import roomescape.theme.exception.ThemeErrorCode
|
||||||
|
import roomescape.theme.exception.ThemeException
|
||||||
|
import roomescape.theme.implement.ThemeFinder
|
||||||
|
import roomescape.time.exception.TimeErrorCode
|
||||||
|
import roomescape.time.exception.TimeException
|
||||||
|
import roomescape.time.infrastructure.persistence.TimeRepository
|
||||||
|
import roomescape.util.TimeFixture
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalTime
|
||||||
|
|
||||||
|
class TimeFinderTest : FunSpec({
|
||||||
|
val timeRepository: TimeRepository = mockk()
|
||||||
|
val reservationFinder: ReservationFinder = mockk()
|
||||||
|
val themeFinder: ThemeFinder = mockk()
|
||||||
|
|
||||||
|
val timeFinder = TimeFinder(timeRepository, reservationFinder, themeFinder)
|
||||||
|
|
||||||
|
context("findAll") {
|
||||||
|
test("모든 시간을 조회한다.") {
|
||||||
|
every {
|
||||||
|
timeRepository.findAll()
|
||||||
|
} returns listOf(mockk(), mockk(), mockk())
|
||||||
|
|
||||||
|
timeFinder.findAll() shouldHaveSize 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("findById") {
|
||||||
|
val timeId = 1L
|
||||||
|
test("동일한 ID인 시간을 찾아 응답한다.") {
|
||||||
|
every {
|
||||||
|
timeRepository.findByIdOrNull(timeId)
|
||||||
|
} returns mockk()
|
||||||
|
|
||||||
|
timeFinder.findById(timeId)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
timeRepository.findByIdOrNull(timeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("동일한 ID인 시간이 없으면 실패한다.") {
|
||||||
|
every {
|
||||||
|
timeRepository.findByIdOrNull(timeId)
|
||||||
|
} returns null
|
||||||
|
|
||||||
|
shouldThrow<TimeException> {
|
||||||
|
timeFinder.findById(timeId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe TimeErrorCode.TIME_NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("findAllWithAvailabilityByDateAndThemeId") {
|
||||||
|
val date = LocalDate.now()
|
||||||
|
val themeId = 1L
|
||||||
|
|
||||||
|
test("테마를 찾을 수 없으면 실패한다.") {
|
||||||
|
every {
|
||||||
|
themeFinder.findById(themeId)
|
||||||
|
} throws ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
|
||||||
|
|
||||||
|
shouldThrow<ThemeException> {
|
||||||
|
timeFinder.findAllWithAvailabilityByDateAndThemeId(date, themeId)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe ThemeErrorCode.THEME_NOT_FOUND
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("날짜, 테마에 맞는 예약 자체가 없으면 모든 시간이 예약 가능하다.") {
|
||||||
|
every {
|
||||||
|
themeFinder.findById(themeId)
|
||||||
|
} returns mockk()
|
||||||
|
|
||||||
|
every {
|
||||||
|
reservationFinder.findAllByDateAndTheme(date, any())
|
||||||
|
} returns emptyList()
|
||||||
|
|
||||||
|
every {
|
||||||
|
timeRepository.findAll()
|
||||||
|
} returns listOf(
|
||||||
|
TimeFixture.create(startAt = LocalTime.now()),
|
||||||
|
TimeFixture.create(startAt = LocalTime.now().plusMinutes(30))
|
||||||
|
)
|
||||||
|
|
||||||
|
val result: List<TimeWithAvailability> =
|
||||||
|
timeFinder.findAllWithAvailabilityByDateAndThemeId(date, themeId)
|
||||||
|
|
||||||
|
assertSoftly(result) {
|
||||||
|
it shouldHaveSize 2
|
||||||
|
it.all { time -> time.isReservable }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
package roomescape.time.implement
|
||||||
|
|
||||||
|
import io.kotest.assertions.throwables.shouldNotThrow
|
||||||
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import roomescape.reservation.implement.ReservationFinder
|
||||||
|
import roomescape.time.exception.TimeErrorCode
|
||||||
|
import roomescape.time.exception.TimeException
|
||||||
|
import roomescape.time.infrastructure.persistence.TimeEntity
|
||||||
|
import roomescape.time.infrastructure.persistence.TimeRepository
|
||||||
|
import roomescape.util.TimeFixture
|
||||||
|
import java.time.LocalTime
|
||||||
|
|
||||||
|
class TimeValidatorTest : FunSpec({
|
||||||
|
val timeRepository: TimeRepository = mockk()
|
||||||
|
val reservationFinder: ReservationFinder = mockk()
|
||||||
|
|
||||||
|
val timeValidator = TimeValidator(timeRepository, reservationFinder)
|
||||||
|
|
||||||
|
context("validateIsAlreadyExists") {
|
||||||
|
val startAt = LocalTime.now()
|
||||||
|
|
||||||
|
test("같은 이메일을 가진 회원이 있으면 예외를 던진다.") {
|
||||||
|
every {
|
||||||
|
timeRepository.existsByStartAt(startAt)
|
||||||
|
} returns true
|
||||||
|
|
||||||
|
shouldThrow<TimeException> {
|
||||||
|
timeValidator.validateIsAlreadyExists(startAt)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe TimeErrorCode.TIME_DUPLICATED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("같은 이메일을 가진 회원이 없으면 종료한다.") {
|
||||||
|
every {
|
||||||
|
timeRepository.existsByStartAt(startAt)
|
||||||
|
} returns false
|
||||||
|
|
||||||
|
shouldNotThrow<TimeException> {
|
||||||
|
timeValidator.validateIsAlreadyExists(startAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("validateIsReserved") {
|
||||||
|
val time: TimeEntity = TimeFixture.create(startAt = LocalTime.now())
|
||||||
|
|
||||||
|
test("해당 시간에 예약이 있으면 예외를 던진다.") {
|
||||||
|
every {
|
||||||
|
reservationFinder.isTimeReserved(time)
|
||||||
|
} returns true
|
||||||
|
|
||||||
|
shouldThrow<TimeException> {
|
||||||
|
timeValidator.validateIsReserved(time)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe TimeErrorCode.TIME_ALREADY_RESERVED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("해당 시간에 예약이 없으면 종료한다.") {
|
||||||
|
every {
|
||||||
|
reservationFinder.isTimeReserved(time)
|
||||||
|
} returns false
|
||||||
|
|
||||||
|
shouldNotThrow<TimeException> {
|
||||||
|
timeValidator.validateIsReserved(time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
84
src/test/kotlin/roomescape/time/implement/TimeWriterTest.kt
Normal file
84
src/test/kotlin/roomescape/time/implement/TimeWriterTest.kt
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package roomescape.time.implement
|
||||||
|
|
||||||
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.*
|
||||||
|
import roomescape.time.exception.TimeErrorCode
|
||||||
|
import roomescape.time.exception.TimeException
|
||||||
|
import roomescape.time.infrastructure.persistence.TimeEntity
|
||||||
|
import roomescape.time.infrastructure.persistence.TimeRepository
|
||||||
|
import roomescape.util.TimeFixture
|
||||||
|
import roomescape.util.TsidFactory
|
||||||
|
import java.time.LocalTime
|
||||||
|
|
||||||
|
class TimeWriterTest : FunSpec({
|
||||||
|
|
||||||
|
val timeValidator: TimeValidator = mockk()
|
||||||
|
val timeRepository: TimeRepository = mockk()
|
||||||
|
|
||||||
|
val timeWriter = TimeWriter(timeValidator, timeRepository, TsidFactory)
|
||||||
|
|
||||||
|
context("create") {
|
||||||
|
val startAt = LocalTime.now()
|
||||||
|
|
||||||
|
test("중복된 시간이 있으면 실패한다.") {
|
||||||
|
every {
|
||||||
|
timeValidator.validateIsAlreadyExists(startAt)
|
||||||
|
} throws TimeException(TimeErrorCode.TIME_DUPLICATED)
|
||||||
|
|
||||||
|
shouldThrow<TimeException> {
|
||||||
|
timeWriter.create(startAt)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe TimeErrorCode.TIME_DUPLICATED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("중복된 시간이 없으면 저장한다.") {
|
||||||
|
every {
|
||||||
|
timeValidator.validateIsAlreadyExists(startAt)
|
||||||
|
} just Runs
|
||||||
|
|
||||||
|
every {
|
||||||
|
timeRepository.save(any())
|
||||||
|
} returns TimeFixture.create(startAt = startAt)
|
||||||
|
|
||||||
|
timeWriter.create(startAt)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
timeRepository.save(any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("delete") {
|
||||||
|
val time: TimeEntity = TimeFixture.create()
|
||||||
|
test("예약이 있는 시간이면 실패한다.") {
|
||||||
|
every {
|
||||||
|
timeValidator.validateIsReserved(time)
|
||||||
|
} throws TimeException(TimeErrorCode.TIME_ALREADY_RESERVED)
|
||||||
|
|
||||||
|
shouldThrow<TimeException> {
|
||||||
|
timeWriter.delete(time)
|
||||||
|
}.also {
|
||||||
|
it.errorCode shouldBe TimeErrorCode.TIME_ALREADY_RESERVED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("예약이 없는 시간이면 제거한다.") {
|
||||||
|
every {
|
||||||
|
timeValidator.validateIsReserved(time)
|
||||||
|
} just Runs
|
||||||
|
|
||||||
|
every {
|
||||||
|
timeRepository.delete(time)
|
||||||
|
} just Runs
|
||||||
|
|
||||||
|
timeWriter.delete(time)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
timeRepository.delete(time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -1,62 +1,46 @@
|
|||||||
package roomescape.time.web
|
package roomescape.time.web
|
||||||
|
|
||||||
import com.ninjasquad.springmockk.MockkBean
|
import com.ninjasquad.springmockk.MockkBean
|
||||||
import com.ninjasquad.springmockk.SpykBean
|
|
||||||
import io.kotest.assertions.assertSoftly
|
import io.kotest.assertions.assertSoftly
|
||||||
import io.kotest.matchers.collections.shouldHaveSize
|
import io.kotest.matchers.collections.shouldHaveSize
|
||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import org.hamcrest.Matchers.equalTo
|
import org.hamcrest.Matchers.equalTo
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||||
import org.springframework.context.annotation.Import
|
|
||||||
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext
|
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import roomescape.auth.exception.AuthErrorCode
|
import roomescape.auth.exception.AuthErrorCode
|
||||||
import roomescape.common.config.JacksonConfig
|
import roomescape.theme.exception.ThemeErrorCode
|
||||||
import roomescape.reservation.infrastructure.persistence.ReservationRepository
|
import roomescape.theme.exception.ThemeException
|
||||||
import roomescape.time.business.TimeService
|
import roomescape.time.business.TimeService
|
||||||
import roomescape.time.exception.TimeErrorCode
|
import roomescape.time.exception.TimeErrorCode
|
||||||
import roomescape.time.infrastructure.persistence.TimeEntity
|
import roomescape.time.exception.TimeException
|
||||||
import roomescape.time.infrastructure.persistence.TimeRepository
|
|
||||||
import roomescape.util.ReservationFixture
|
|
||||||
import roomescape.util.RoomescapeApiTest
|
import roomescape.util.RoomescapeApiTest
|
||||||
import roomescape.util.ThemeFixture
|
|
||||||
import roomescape.util.TimeFixture
|
import roomescape.util.TimeFixture
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
|
|
||||||
@WebMvcTest(TimeController::class)
|
@WebMvcTest(TimeController::class)
|
||||||
class TimeControllerTest(
|
class TimeControllerTest(val mockMvc: MockMvc) : RoomescapeApiTest() {
|
||||||
val mockMvc: MockMvc,
|
@MockkBean
|
||||||
) : RoomescapeApiTest() {
|
|
||||||
|
|
||||||
@SpykBean
|
|
||||||
private lateinit var timeService: TimeService
|
private lateinit var timeService: TimeService
|
||||||
|
|
||||||
@MockkBean
|
|
||||||
private lateinit var timeRepository: TimeRepository
|
|
||||||
|
|
||||||
@MockkBean
|
|
||||||
private lateinit var reservationRepository: ReservationRepository
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Given("등록된 모든 시간을 조회할 때") {
|
Given("GET /times 요청을") {
|
||||||
val endpoint = "/times"
|
val endpoint = "/times"
|
||||||
|
|
||||||
When("관리자인 경우") {
|
When("관리자가 보내면") {
|
||||||
beforeTest {
|
beforeTest {
|
||||||
loginAsAdmin()
|
loginAsAdmin()
|
||||||
}
|
}
|
||||||
|
|
||||||
Then("정상 응답") {
|
Then("정상 응답") {
|
||||||
every {
|
every {
|
||||||
timeRepository.findAll()
|
timeService.findTimes()
|
||||||
} returns listOf(
|
} returns listOf(
|
||||||
TimeFixture.create(id = 1L),
|
TimeFixture.create(id = 1L),
|
||||||
TimeFixture.create(id = 2L)
|
TimeFixture.create(id = 2L)
|
||||||
)
|
).toResponse()
|
||||||
|
|
||||||
runGetTest(
|
runGetTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
@ -72,11 +56,11 @@ class TimeControllerTest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
When("관리자가 아닌 경우") {
|
When("관리자가 보내지 않았다면") {
|
||||||
loginAsUser()
|
loginAsUser()
|
||||||
val expectedError = AuthErrorCode.ACCESS_DENIED
|
val expectedError = AuthErrorCode.ACCESS_DENIED
|
||||||
|
|
||||||
Then("에러 응답을 받는다.") {
|
Then("예외 응답") {
|
||||||
runGetTest(
|
runGetTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
endpoint = endpoint,
|
endpoint = endpoint,
|
||||||
@ -85,17 +69,17 @@ class TimeControllerTest(
|
|||||||
}.andExpect {
|
}.andExpect {
|
||||||
content {
|
content {
|
||||||
contentType(MediaType.APPLICATION_JSON)
|
contentType(MediaType.APPLICATION_JSON)
|
||||||
jsonPath("$.code") { value(expectedError.errorCode) }
|
jsonPath("$.code") { equalTo(expectedError.errorCode) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Given("시간을 추가할 때") {
|
Given("POST /times 요청을") {
|
||||||
val endpoint = "/times"
|
val endpoint = "/times"
|
||||||
|
|
||||||
When("관리자인 경우") {
|
When("관리자가 보낼 때") {
|
||||||
beforeTest {
|
beforeTest {
|
||||||
loginAsAdmin()
|
loginAsAdmin()
|
||||||
}
|
}
|
||||||
@ -139,8 +123,8 @@ class TimeControllerTest(
|
|||||||
Then("동일한 시간이 존재하면 예외 응답") {
|
Then("동일한 시간이 존재하면 예외 응답") {
|
||||||
val expectedError = TimeErrorCode.TIME_DUPLICATED
|
val expectedError = TimeErrorCode.TIME_DUPLICATED
|
||||||
every {
|
every {
|
||||||
timeRepository.existsByStartAt(time)
|
timeService.createTime(request)
|
||||||
} returns true
|
} throws TimeException(expectedError)
|
||||||
|
|
||||||
runPostTest(
|
runPostTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
@ -150,16 +134,16 @@ class TimeControllerTest(
|
|||||||
status { isEqualTo(expectedError.httpStatus.value()) }
|
status { isEqualTo(expectedError.httpStatus.value()) }
|
||||||
content {
|
content {
|
||||||
contentType(MediaType.APPLICATION_JSON)
|
contentType(MediaType.APPLICATION_JSON)
|
||||||
jsonPath("$.code") { value(expectedError.errorCode) }
|
jsonPath("$.code") { equalTo(expectedError.errorCode) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
When("관리자가 아닌 경우") {
|
When("관리자가 보내지 않았다면") {
|
||||||
loginAsUser()
|
loginAsUser()
|
||||||
|
|
||||||
Then("에러 응답을 받는다.") {
|
Then("예외 응답") {
|
||||||
val expectedError = AuthErrorCode.ACCESS_DENIED
|
val expectedError = AuthErrorCode.ACCESS_DENIED
|
||||||
runPostTest(
|
runPostTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
@ -173,10 +157,10 @@ class TimeControllerTest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Given("시간을 삭제할 때") {
|
Given("DELETE /times/{id} 요청을") {
|
||||||
val endpoint = "/times/1"
|
val endpoint = "/times/1"
|
||||||
|
|
||||||
When("관리자인 경우") {
|
When("관리자가 보낼 때") {
|
||||||
beforeTest {
|
beforeTest {
|
||||||
loginAsAdmin()
|
loginAsAdmin()
|
||||||
}
|
}
|
||||||
@ -197,9 +181,10 @@ class TimeControllerTest(
|
|||||||
Then("없는 시간을 조회하면 예외 응답") {
|
Then("없는 시간을 조회하면 예외 응답") {
|
||||||
val id = 1L
|
val id = 1L
|
||||||
val expectedError = TimeErrorCode.TIME_NOT_FOUND
|
val expectedError = TimeErrorCode.TIME_NOT_FOUND
|
||||||
|
|
||||||
every {
|
every {
|
||||||
timeRepository.findByIdOrNull(id)
|
timeService.deleteTime(id)
|
||||||
} returns null
|
} throws TimeException(expectedError)
|
||||||
|
|
||||||
runDeleteTest(
|
runDeleteTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
@ -208,7 +193,7 @@ class TimeControllerTest(
|
|||||||
status { isEqualTo(expectedError.httpStatus.value()) }
|
status { isEqualTo(expectedError.httpStatus.value()) }
|
||||||
content {
|
content {
|
||||||
contentType(MediaType.APPLICATION_JSON)
|
contentType(MediaType.APPLICATION_JSON)
|
||||||
jsonPath("$.code") { value(expectedError.errorCode) }
|
jsonPath("$.code") { equalTo(expectedError.errorCode) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -216,13 +201,10 @@ class TimeControllerTest(
|
|||||||
Then("예약이 있는 시간을 삭제하면 예외 응답") {
|
Then("예약이 있는 시간을 삭제하면 예외 응답") {
|
||||||
val id = 1L
|
val id = 1L
|
||||||
val expectedError = TimeErrorCode.TIME_ALREADY_RESERVED
|
val expectedError = TimeErrorCode.TIME_ALREADY_RESERVED
|
||||||
every {
|
|
||||||
timeRepository.findByIdOrNull(id)
|
|
||||||
} returns TimeFixture.create(id = id)
|
|
||||||
|
|
||||||
every {
|
every {
|
||||||
reservationRepository.findAllByTime(any())
|
timeService.deleteTime(id)
|
||||||
} returns listOf(ReservationFixture.create())
|
} throws TimeException(expectedError)
|
||||||
|
|
||||||
runDeleteTest(
|
runDeleteTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
@ -231,16 +213,16 @@ class TimeControllerTest(
|
|||||||
status { isEqualTo(expectedError.httpStatus.value()) }
|
status { isEqualTo(expectedError.httpStatus.value()) }
|
||||||
content {
|
content {
|
||||||
contentType(MediaType.APPLICATION_JSON)
|
contentType(MediaType.APPLICATION_JSON)
|
||||||
jsonPath("$.code") { value(expectedError.errorCode) }
|
jsonPath("$.code") { equalTo(expectedError.errorCode) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
When("관리자가 아닌 경우") {
|
When("관리자가 보내지 않았다면") {
|
||||||
loginAsUser()
|
loginAsUser()
|
||||||
|
|
||||||
Then("에러 응답을 받는다.") {
|
Then("예외 응답") {
|
||||||
val expectedError = AuthErrorCode.ACCESS_DENIED
|
val expectedError = AuthErrorCode.ACCESS_DENIED
|
||||||
runDeleteTest(
|
runDeleteTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
@ -253,36 +235,28 @@ class TimeControllerTest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Given("날짜, 테마가 주어졌을 때") {
|
Given("GET /times/search?date={date}&themeId={themeId} 요청을 ") {
|
||||||
loginAsUser()
|
|
||||||
|
|
||||||
val date: LocalDate = LocalDate.now()
|
val date: LocalDate = LocalDate.now()
|
||||||
val themeId = 1L
|
val themeId = 1L
|
||||||
|
|
||||||
When("저장된 예약 시간이 있으면") {
|
When("회원이 보낼 때") {
|
||||||
val times: List<TimeEntity> = listOf(
|
beforeTest {
|
||||||
TimeFixture.create(id = 1L, startAt = LocalTime.of(10, 0)),
|
loginAsUser()
|
||||||
TimeFixture.create(id = 2L, startAt = LocalTime.of(11, 0))
|
}
|
||||||
)
|
|
||||||
|
|
||||||
every {
|
Then("정상 응답") {
|
||||||
timeRepository.findAll()
|
val response = TimeWithAvailabilityListResponse(
|
||||||
} returns times
|
listOf(
|
||||||
|
TimeWithAvailabilityResponse(1L, LocalTime.of(10, 0), true),
|
||||||
Then("그 시간과, 해당 날짜와 테마에 대한 예약 여부가 담긴 목록을 응답") {
|
TimeWithAvailabilityResponse(2L, LocalTime.of(10, 1), false),
|
||||||
|
TimeWithAvailabilityResponse(3L, LocalTime.of(10, 2), true)
|
||||||
every {
|
|
||||||
reservationRepository.findByDateAndThemeId(date, themeId)
|
|
||||||
} returns listOf(
|
|
||||||
ReservationFixture.create(
|
|
||||||
id = 1L,
|
|
||||||
date = date,
|
|
||||||
theme = ThemeFixture.create(id = themeId),
|
|
||||||
time = times[0]
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
every {
|
||||||
|
timeService.findTimesWithAvailability(date, themeId)
|
||||||
|
} returns response
|
||||||
|
|
||||||
val response = runGetTest(
|
val result = runGetTest(
|
||||||
mockMvc = mockMvc,
|
mockMvc = mockMvc,
|
||||||
endpoint = "/times/search?date=$date&themeId=$themeId",
|
endpoint = "/times/search?date=$date&themeId=$themeId",
|
||||||
) {
|
) {
|
||||||
@ -292,15 +266,46 @@ class TimeControllerTest(
|
|||||||
}
|
}
|
||||||
}.andReturn().readValue(TimeWithAvailabilityListResponse::class.java)
|
}.andReturn().readValue(TimeWithAvailabilityListResponse::class.java)
|
||||||
|
|
||||||
assertSoftly(response.times) {
|
assertSoftly(result.times) {
|
||||||
this shouldHaveSize times.size
|
this shouldHaveSize response.times.size
|
||||||
this[0].id shouldBe times[0].id
|
this[0].id shouldBe response.times[0].id
|
||||||
this[0].isAvailable shouldBe false
|
this[0].isAvailable shouldBe response.times[0].isAvailable
|
||||||
|
|
||||||
this[1].id shouldBe times[1].id
|
this[1].id shouldBe response.times[1].id
|
||||||
this[1].isAvailable shouldBe true
|
this[1].isAvailable shouldBe response.times[1].isAvailable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Then("테마를 찾을 수 없으면 예외 응답") {
|
||||||
|
val expectedError = ThemeErrorCode.THEME_NOT_FOUND
|
||||||
|
every {
|
||||||
|
timeService.findTimesWithAvailability(date, themeId)
|
||||||
|
} throws ThemeException(expectedError)
|
||||||
|
|
||||||
|
runGetTest(
|
||||||
|
mockMvc = mockMvc,
|
||||||
|
endpoint = "/times/search?date=$date&themeId=$themeId",
|
||||||
|
) {
|
||||||
|
status { isNotFound() }
|
||||||
|
jsonPath("$.code", equalTo(expectedError.errorCode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
When("비회원이 보내면") {
|
||||||
|
doNotLogin()
|
||||||
|
|
||||||
|
Then("예외 응답") {
|
||||||
|
val expectedError = AuthErrorCode.MEMBER_NOT_FOUND
|
||||||
|
|
||||||
|
runGetTest(
|
||||||
|
mockMvc = mockMvc,
|
||||||
|
endpoint = "/times/search?date=$date&themeId=$themeId",
|
||||||
|
) {
|
||||||
|
status { isEqualTo(expectedError.httpStatus.value()) }
|
||||||
|
jsonPath("$.code", equalTo(expectedError.errorCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import roomescape.payment.web.PaymentCancelRequest
|
|||||||
import roomescape.payment.web.PaymentCancelResponse
|
import roomescape.payment.web.PaymentCancelResponse
|
||||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||||
|
import roomescape.reservation.web.AdminReservationCreateRequest
|
||||||
import roomescape.reservation.web.ReservationCreateWithPaymentRequest
|
import roomescape.reservation.web.ReservationCreateWithPaymentRequest
|
||||||
import roomescape.reservation.web.WaitingCreateRequest
|
import roomescape.reservation.web.WaitingCreateRequest
|
||||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||||
@ -22,7 +23,6 @@ import java.time.LocalDate
|
|||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
|
||||||
val TsidFactory: TsidFactory = TsidFactory(0)
|
val TsidFactory: TsidFactory = TsidFactory(0)
|
||||||
|
|
||||||
object MemberFixture {
|
object MemberFixture {
|
||||||
@ -103,6 +103,18 @@ object ReservationFixture {
|
|||||||
paymentType = paymentType
|
paymentType = paymentType
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun createAdminRequest(
|
||||||
|
date: LocalDate = LocalDate.now().plusWeeks(1),
|
||||||
|
themeId: Long = 1L,
|
||||||
|
timeId: Long = 1L,
|
||||||
|
memberId: Long = 1L
|
||||||
|
): AdminReservationCreateRequest = AdminReservationCreateRequest(
|
||||||
|
date = date,
|
||||||
|
timeId = timeId,
|
||||||
|
themeId = themeId,
|
||||||
|
memberId = memberId
|
||||||
|
)
|
||||||
|
|
||||||
fun createWaitingRequest(
|
fun createWaitingRequest(
|
||||||
date: LocalDate = LocalDate.now().plusWeeks(1),
|
date: LocalDate = LocalDate.now().plusWeeks(1),
|
||||||
themeId: Long = 1L,
|
themeId: Long = 1L,
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import org.springframework.context.annotation.Bean
|
|||||||
import org.springframework.context.annotation.Import
|
import org.springframework.context.annotation.Import
|
||||||
import org.springframework.context.annotation.Primary
|
import org.springframework.context.annotation.Primary
|
||||||
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext
|
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
|
||||||
import org.springframework.http.HttpHeaders
|
import org.springframework.http.HttpHeaders
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.test.web.servlet.*
|
import org.springframework.test.web.servlet.*
|
||||||
@ -22,7 +21,9 @@ import roomescape.auth.web.support.AuthInterceptor
|
|||||||
import roomescape.auth.web.support.MemberIdResolver
|
import roomescape.auth.web.support.MemberIdResolver
|
||||||
import roomescape.common.config.JacksonConfig
|
import roomescape.common.config.JacksonConfig
|
||||||
import roomescape.common.log.ApiLogMessageConverter
|
import roomescape.common.log.ApiLogMessageConverter
|
||||||
import roomescape.member.business.MemberService
|
import roomescape.member.exception.MemberErrorCode
|
||||||
|
import roomescape.member.exception.MemberException
|
||||||
|
import roomescape.member.implement.MemberFinder
|
||||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||||
import roomescape.member.infrastructure.persistence.MemberRepository
|
import roomescape.member.infrastructure.persistence.MemberRepository
|
||||||
import roomescape.util.MemberFixture.NOT_LOGGED_IN_USERID
|
import roomescape.util.MemberFixture.NOT_LOGGED_IN_USERID
|
||||||
@ -37,14 +38,14 @@ abstract class RoomescapeApiTest : BehaviorSpec() {
|
|||||||
@SpykBean
|
@SpykBean
|
||||||
private lateinit var memberIdResolver: MemberIdResolver
|
private lateinit var memberIdResolver: MemberIdResolver
|
||||||
|
|
||||||
@SpykBean
|
@MockkBean
|
||||||
lateinit var memberService: MemberService
|
private lateinit var memberRepository: MemberRepository
|
||||||
|
|
||||||
@SpykBean
|
@SpykBean
|
||||||
lateinit var apiLogMessageConverter: ApiLogMessageConverter
|
lateinit var apiLogMessageConverter: ApiLogMessageConverter
|
||||||
|
|
||||||
@MockkBean
|
@SpykBean
|
||||||
lateinit var memberRepository: MemberRepository
|
lateinit var memberFinder: MemberFinder
|
||||||
|
|
||||||
@MockkBean
|
@MockkBean
|
||||||
lateinit var jwtHandler: JwtHandler
|
lateinit var jwtHandler: JwtHandler
|
||||||
@ -96,8 +97,7 @@ abstract class RoomescapeApiTest : BehaviorSpec() {
|
|||||||
jwtHandler.getMemberIdFromToken(any())
|
jwtHandler.getMemberIdFromToken(any())
|
||||||
} returns admin.id!!
|
} returns admin.id!!
|
||||||
|
|
||||||
every { memberRepository.existsById(admin.id!!) } returns true
|
every { memberFinder.findById(admin.id!!) } returns admin
|
||||||
every { memberRepository.findByIdOrNull(admin.id!!) } returns admin
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loginAsUser() {
|
fun loginAsUser() {
|
||||||
@ -105,8 +105,7 @@ abstract class RoomescapeApiTest : BehaviorSpec() {
|
|||||||
jwtHandler.getMemberIdFromToken(any())
|
jwtHandler.getMemberIdFromToken(any())
|
||||||
} returns user.id!!
|
} returns user.id!!
|
||||||
|
|
||||||
every { memberRepository.existsById(user.id!!) } returns true
|
every { memberFinder.findById(user.id!!) } returns user
|
||||||
every { memberRepository.findByIdOrNull(user.id!!) } returns user
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun doNotLogin() {
|
fun doNotLogin() {
|
||||||
@ -114,8 +113,9 @@ abstract class RoomescapeApiTest : BehaviorSpec() {
|
|||||||
jwtHandler.getMemberIdFromToken(any())
|
jwtHandler.getMemberIdFromToken(any())
|
||||||
} throws AuthException(AuthErrorCode.INVALID_TOKEN)
|
} throws AuthException(AuthErrorCode.INVALID_TOKEN)
|
||||||
|
|
||||||
every { memberRepository.existsById(NOT_LOGGED_IN_USERID) } returns false
|
every {
|
||||||
every { memberRepository.findByIdOrNull(NOT_LOGGED_IN_USERID) } returns null
|
memberFinder.findById(NOT_LOGGED_IN_USERID)
|
||||||
|
} throws MemberException(MemberErrorCode.MEMBER_NOT_FOUND)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> MvcResult.readValue(valueType: Class<T>): T = this.response.contentAsString
|
fun <T> MvcResult.readValue(valueType: Class<T>): T = this.response.contentAsString
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user