feat(gift): 선물 관리자 API를 추가한다
This commit is contained in:
+74
@@ -0,0 +1,74 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.`in`.web
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftCategoryRequest
|
||||||
|
import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftCategoryResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftMarkUndeliverableRequest
|
||||||
|
import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftOperationStatusResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftSizePriceRequest
|
||||||
|
import kr.co.vividnext.sodalive.v2.api.admin.gift.dto.AdminGiftSizePriceResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.application.GiftAdminService
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.domain.GiftSize
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v2/admin")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
class AdminGiftController(private val service: GiftAdminService) {
|
||||||
|
@GetMapping("/gift-categories")
|
||||||
|
fun getCategories() = ApiResponse.ok(service.getCategories().map { AdminGiftCategoryResponse.from(it) })
|
||||||
|
|
||||||
|
@PostMapping("/gift-categories")
|
||||||
|
fun createCategory(@RequestBody request: AdminGiftCategoryRequest) = ApiResponse.ok(
|
||||||
|
AdminGiftCategoryResponse.from(service.createCategory(request.toCommand()))
|
||||||
|
)
|
||||||
|
|
||||||
|
@PutMapping("/gift-categories/{categoryId}")
|
||||||
|
fun updateCategory(
|
||||||
|
@PathVariable categoryId: Long,
|
||||||
|
@RequestBody request: AdminGiftCategoryRequest
|
||||||
|
) = ApiResponse.ok(AdminGiftCategoryResponse.from(service.updateCategory(categoryId, request.toCommand())))
|
||||||
|
|
||||||
|
@DeleteMapping("/gift-categories/{categoryId}")
|
||||||
|
fun deleteCategory(@PathVariable categoryId: Long) = ApiResponse.ok(
|
||||||
|
AdminGiftCategoryResponse.from(service.deleteCategory(categoryId))
|
||||||
|
)
|
||||||
|
|
||||||
|
@GetMapping("/gift-size-prices")
|
||||||
|
fun getSizePrices() = ApiResponse.ok(service.getSizePrices().map { AdminGiftSizePriceResponse.from(it) })
|
||||||
|
|
||||||
|
@PutMapping("/gift-size-prices/{sizeCode}")
|
||||||
|
fun updateSizePrice(
|
||||||
|
@PathVariable sizeCode: GiftSize,
|
||||||
|
@RequestBody request: AdminGiftSizePriceRequest
|
||||||
|
) = ApiResponse.ok(AdminGiftSizePriceResponse.from(service.updateSizePrice(sizeCode, request.toCommand())))
|
||||||
|
|
||||||
|
@PostMapping("/gifts/{applicationNo}/arrive-mailbox")
|
||||||
|
fun arriveMailbox(@PathVariable applicationNo: String) = ApiResponse.ok(
|
||||||
|
AdminGiftOperationStatusResponse.from(service.arriveMailbox(applicationNo))
|
||||||
|
)
|
||||||
|
|
||||||
|
@PostMapping("/gifts/{applicationNo}/complete-inspection")
|
||||||
|
fun completeInspection(@PathVariable applicationNo: String) = ApiResponse.ok(
|
||||||
|
AdminGiftOperationStatusResponse.from(service.completeInspection(applicationNo))
|
||||||
|
)
|
||||||
|
|
||||||
|
@PostMapping("/gifts/{applicationNo}/mark-undeliverable")
|
||||||
|
fun markUndeliverable(
|
||||||
|
@PathVariable applicationNo: String,
|
||||||
|
@RequestBody request: AdminGiftMarkUndeliverableRequest
|
||||||
|
) = ApiResponse.ok(AdminGiftOperationStatusResponse.from(service.markUndeliverable(applicationNo, request.reason)))
|
||||||
|
|
||||||
|
@PostMapping("/gifts/{applicationNo}/complete-delivery")
|
||||||
|
fun completeDelivery(@PathVariable applicationNo: String) = ApiResponse.ok(
|
||||||
|
AdminGiftOperationStatusResponse.from(service.completeDelivery(applicationNo))
|
||||||
|
)
|
||||||
|
}
|
||||||
+224
@@ -0,0 +1,224 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.`in`.web
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.Gift
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDelivery
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDeliveryRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePrice
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePriceRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.domain.GiftSize
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.domain.GiftStatus
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
||||||
|
import org.springframework.test.annotation.DirtiesContext
|
||||||
|
import org.springframework.test.context.ContextConfiguration
|
||||||
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
|
||||||
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
|
||||||
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
|
||||||
|
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
|
||||||
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
|
||||||
|
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@SpringBootTest(
|
||||||
|
properties = [
|
||||||
|
"cloud.aws.cloud-front.host=https://cdn.test",
|
||||||
|
"spring.datasource.url=jdbc:h2:mem:gift-admin-controller;MODE=MySQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=VALUE;" +
|
||||||
|
"DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
|
||||||
|
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
|
||||||
|
@DisplayName("선물 관리자 API 테스트")
|
||||||
|
class AdminGiftControllerTest @Autowired constructor(
|
||||||
|
private val mockMvc: MockMvc,
|
||||||
|
private val categoryRepository: GiftCategoryRepository,
|
||||||
|
private val sizePriceRepository: GiftSizePriceRepository,
|
||||||
|
private val giftRepository: GiftRepository,
|
||||||
|
private val giftDeliveryRepository: GiftDeliveryRepository
|
||||||
|
) {
|
||||||
|
@BeforeEach
|
||||||
|
fun deleteGiftFixtures() {
|
||||||
|
giftDeliveryRepository.deleteAll()
|
||||||
|
giftRepository.deleteAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("관리자는 카테고리를 등록, 조회, 수정, 논리삭제한다")
|
||||||
|
fun shouldManageGiftCategories() {
|
||||||
|
mockMvc.perform(
|
||||||
|
post("/api/v2/admin/gift-categories")
|
||||||
|
.with(user("admin").roles("ADMIN"))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(
|
||||||
|
"""
|
||||||
|
{"categoryCode":"DOLL","name":"인형","requiresDamageWaiver":true,"isActive":true}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.success").value(true))
|
||||||
|
.andExpect(jsonPath("$.data.categoryCode").value("DOLL"))
|
||||||
|
|
||||||
|
val categoryId = categoryRepository.findByCategoryCode("DOLL")!!.id!!
|
||||||
|
|
||||||
|
mockMvc.perform(
|
||||||
|
put("/api/v2/admin/gift-categories/$categoryId")
|
||||||
|
.with(user("admin").roles("ADMIN"))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(
|
||||||
|
"""
|
||||||
|
{"categoryCode":"DOLL","name":"큰 인형","requiresDamageWaiver":false,"isActive":true}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.data.name").value("큰 인형"))
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/v2/admin/gift-categories").with(user("admin").roles("ADMIN")))
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.data[0].name").value("큰 인형"))
|
||||||
|
|
||||||
|
mockMvc.perform(delete("/api/v2/admin/gift-categories/$categoryId").with(user("admin").roles("ADMIN")))
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.data.isActive").value(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("관리자는 사이즈 가격을 조회하고 수정하며 잘못된 가격을 거부한다")
|
||||||
|
fun shouldManageGiftSizePrices() {
|
||||||
|
sizePriceRepository.save(GiftSizePrice(sizeCode = GiftSize.SMALL, basePriceCan = 100, salePriceCan = 100))
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/v2/admin/gift-size-prices").with(user("admin").roles("ADMIN")))
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.data[0].sizeCode").value("SMALL"))
|
||||||
|
|
||||||
|
mockMvc.perform(
|
||||||
|
put("/api/v2/admin/gift-size-prices/SMALL")
|
||||||
|
.with(user("admin").roles("ADMIN"))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("""{"basePriceCan":100,"salePriceCan":80,"isActive":true}""")
|
||||||
|
)
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.data.salePriceCan").value(80))
|
||||||
|
|
||||||
|
mockMvc.perform(
|
||||||
|
put("/api/v2/admin/gift-size-prices/SMALL")
|
||||||
|
.with(user("admin").roles("ADMIN"))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("""{"basePriceCan":100,"salePriceCan":101,"isActive":true}""")
|
||||||
|
)
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.success").value(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("관리자는 사서함 도착과 검수완료를 처리한다")
|
||||||
|
fun shouldUpdateGiftOperationStatuses() {
|
||||||
|
saveGift(GiftStatus.TRACKING_REGISTERED)
|
||||||
|
|
||||||
|
mockMvc.perform(post("/api/v2/admin/gifts/$APPLICATION_NO/arrive-mailbox").with(user("admin").roles("ADMIN")))
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.success").value(true))
|
||||||
|
.andExpect(jsonPath("$.data.applicationNo").value(APPLICATION_NO))
|
||||||
|
.andExpect(jsonPath("$.data.status").value("ARRIVED_AT_MAILBOX"))
|
||||||
|
|
||||||
|
mockMvc.perform(post("/api/v2/admin/gifts/$APPLICATION_NO/complete-inspection").with(user("admin").roles("ADMIN")))
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.success").value(true))
|
||||||
|
.andExpect(jsonPath("$.data.status").value("INSPECTION_COMPLETED"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("관리자는 전달불가 사유를 등록하고 전달불가 처리한다")
|
||||||
|
fun shouldMarkGiftUndeliverable() {
|
||||||
|
saveGift(GiftStatus.INSPECTION_COMPLETED)
|
||||||
|
|
||||||
|
mockMvc.perform(
|
||||||
|
post("/api/v2/admin/gifts/$APPLICATION_NO/mark-undeliverable")
|
||||||
|
.with(user("admin").roles("ADMIN"))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("""{"reason":"선물 접수 규격에 맞지 않습니다."}""")
|
||||||
|
)
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.success").value(true))
|
||||||
|
.andExpect(jsonPath("$.data.applicationNo").value(APPLICATION_NO))
|
||||||
|
.andExpect(jsonPath("$.data.status").value("UNDELIVERABLE"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("관리자는 전달불가 사유 없이 전달불가 처리할 수 없다")
|
||||||
|
fun shouldRejectUndeliverableWithoutReason() {
|
||||||
|
saveGift(GiftStatus.INSPECTION_COMPLETED)
|
||||||
|
|
||||||
|
mockMvc.perform(
|
||||||
|
post("/api/v2/admin/gifts/$APPLICATION_NO/mark-undeliverable")
|
||||||
|
.with(user("admin").roles("ADMIN"))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content("""{"reason":" "}""")
|
||||||
|
)
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.success").value(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("관리자는 검수완료 선물을 전달완료 처리한다")
|
||||||
|
fun shouldCompleteGiftDelivery() {
|
||||||
|
saveGift(GiftStatus.INSPECTION_COMPLETED)
|
||||||
|
|
||||||
|
mockMvc.perform(post("/api/v2/admin/gifts/$APPLICATION_NO/complete-delivery").with(user("admin").roles("ADMIN")))
|
||||||
|
.andExpect(status().isOk)
|
||||||
|
.andExpect(jsonPath("$.success").value(true))
|
||||||
|
.andExpect(jsonPath("$.data.applicationNo").value(APPLICATION_NO))
|
||||||
|
.andExpect(jsonPath("$.data.status").value("DELIVERED"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveGift(status: GiftStatus) {
|
||||||
|
val gift = giftRepository.save(
|
||||||
|
Gift(
|
||||||
|
applicationNo = APPLICATION_NO,
|
||||||
|
senderMemberId = SENDER_MEMBER_ID,
|
||||||
|
recipientMemberId = RECIPIENT_MEMBER_ID,
|
||||||
|
status = status,
|
||||||
|
sizeCode = GiftSize.SMALL,
|
||||||
|
categoryId = 1L,
|
||||||
|
categoryNameSnapshot = "인형",
|
||||||
|
salePriceCan = 80,
|
||||||
|
canUsageId = 10L,
|
||||||
|
senderTermsAgreed = true,
|
||||||
|
senderPrivacyAgreed = true,
|
||||||
|
senderTermsAgreedAt = NOW.minusDays(1),
|
||||||
|
senderPrivacyAgreedAt = NOW.minusDays(1),
|
||||||
|
damageWaiverAgreed = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
giftDeliveryRepository.save(
|
||||||
|
GiftDelivery(
|
||||||
|
giftId = requireNotNull(gift.id),
|
||||||
|
senderName = "홍길동",
|
||||||
|
senderPhoneNumber = "01012345678",
|
||||||
|
senderZipCode = "06234",
|
||||||
|
senderAddress = "서울시 강남구",
|
||||||
|
trackingDeadlineAt = NOW.minusDays(1),
|
||||||
|
trackingRegisteredAt = NOW.minusHours(1)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val APPLICATION_NO = "G20260929000001"
|
||||||
|
const val SENDER_MEMBER_ID = 1L
|
||||||
|
const val RECIPIENT_MEMBER_ID = 2L
|
||||||
|
val NOW: LocalDateTime = LocalDateTime.of(2026, 10, 3, 12, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user