generated from pricelees/issue-pr-template
Compare commits
4 Commits
main
...
refactor/#
| Author | SHA1 | Date | |
|---|---|---|---|
| 22163f8392 | |||
| 4ced6ad3c3 | |||
| f12a930a6c | |||
| 768c47f1ae |
@ -36,14 +36,18 @@ class OrderService(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
fun confirm(reservationId: Long, paymentConfirmRequest: PaymentConfirmRequest) {
|
fun confirm(reservationId: Long, paymentConfirmRequest: PaymentConfirmRequest) {
|
||||||
val trial = paymentAttemptRepository.countByReservationId(reservationId)
|
var trial: Long = 0
|
||||||
val paymentKey = paymentConfirmRequest.paymentKey
|
val paymentKey = paymentConfirmRequest.paymentKey
|
||||||
|
|
||||||
log.info { "[confirm] 결제 및 예약 확정 시작: reservationId=${reservationId}, paymentKey=${paymentKey}" }
|
log.info { "[confirm] 결제 및 예약 확정 시작: reservationId=${reservationId}, paymentKey=${paymentKey}" }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
|
trial = transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
|
||||||
validateAndMarkInProgress(reservationId)
|
getTrialAfterValidateCanConfirm(reservationId).also {
|
||||||
|
reservationService.markInProgress(reservationId)
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
log.warn { "[confirm] 모든 paymentAttempts 조회 과정에서의 예상치 못한 null 응답: reservationId=${reservationId}" }
|
||||||
|
throw OrderException(OrderErrorCode.BOOKING_UNEXPECTED_ERROR)
|
||||||
}
|
}
|
||||||
|
|
||||||
val paymentClientResponse: PaymentGatewayResponse =
|
val paymentClientResponse: PaymentGatewayResponse =
|
||||||
@ -61,20 +65,32 @@ class OrderService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateAndMarkInProgress(reservationId: Long) {
|
private fun getTrialAfterValidateCanConfirm(reservationId: Long): Long {
|
||||||
log.info { "[validateAndMarkInProgress] 예약 확정 가능 여부 검증 시작: reservationId=${reservationId}" }
|
log.info { "[validateAndMarkInProgress] 예약 확정 가능 여부 검증 시작: reservationId=${reservationId}" }
|
||||||
val reservation: ReservationStateResponse = reservationService.findStatusWithLock(reservationId)
|
val reservation: ReservationStateResponse = reservationService.findStatusWithLock(reservationId)
|
||||||
val schedule: ScheduleStateResponse = scheduleService.findStateWithLock(reservation.scheduleId)
|
val schedule: ScheduleStateResponse = scheduleService.findStateWithLock(reservation.scheduleId)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
orderValidator.validateCanConfirm(reservation, schedule)
|
orderValidator.validateCanConfirm(reservation, schedule)
|
||||||
|
|
||||||
|
return getTrialIfSuccessAttemptNotExists(reservationId).also {
|
||||||
log.info { "[validateAndMarkInProgress] 예약 확정 가능 여부 검증 완료: reservationId=${reservationId}" }
|
log.info { "[validateAndMarkInProgress] 예약 확정 가능 여부 검증 완료: reservationId=${reservationId}" }
|
||||||
|
}
|
||||||
} catch (e: OrderException) {
|
} catch (e: OrderException) {
|
||||||
val errorCode = OrderErrorCode.NOT_CONFIRMABLE
|
val errorCode = OrderErrorCode.NOT_CONFIRMABLE
|
||||||
throw OrderException(errorCode, e.message)
|
throw OrderException(errorCode, e.message)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
reservationService.markInProgress(reservationId)
|
private fun getTrialIfSuccessAttemptNotExists(reservationId: Long): Long {
|
||||||
|
val paymentAttempts: List<PaymentAttemptEntity> = paymentAttemptRepository.findAllByReservationId(reservationId)
|
||||||
|
|
||||||
|
if (paymentAttempts.any { it.result == AttemptResult.SUCCESS }) {
|
||||||
|
log.info { "[validateCanConfirm] 이미 결제 완료된 예약: id=${reservationId}" }
|
||||||
|
throw OrderException(OrderErrorCode.BOOKING_ALREADY_COMPLETED)
|
||||||
|
}
|
||||||
|
|
||||||
|
return paymentAttempts.size.toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestConfirmPayment(
|
private fun requestConfirmPayment(
|
||||||
|
|||||||
@ -16,18 +16,11 @@ import java.time.LocalDateTime
|
|||||||
private val log: KLogger = KotlinLogging.logger {}
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class OrderValidator(
|
class OrderValidator {
|
||||||
private val paymentAttemptRepository: PaymentAttemptRepository
|
|
||||||
) {
|
|
||||||
fun validateCanConfirm(
|
fun validateCanConfirm(
|
||||||
reservation: ReservationStateResponse,
|
reservation: ReservationStateResponse,
|
||||||
schedule: ScheduleStateResponse
|
schedule: ScheduleStateResponse
|
||||||
) {
|
) {
|
||||||
if (paymentAttemptRepository.isSuccessAttemptExists(reservation.id)) {
|
|
||||||
log.info { "[validateCanConfirm] 이미 결제 완료된 예약: id=${reservation.id}" }
|
|
||||||
throw OrderException(OrderErrorCode.BOOKING_ALREADY_COMPLETED)
|
|
||||||
}
|
|
||||||
|
|
||||||
validateReservationStatus(reservation)
|
validateReservationStatus(reservation)
|
||||||
validateScheduleStatus(schedule)
|
validateScheduleStatus(schedule)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,4 +23,6 @@ interface PaymentAttemptRepository: JpaRepository<PaymentAttemptEntity, Long> {
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun isSuccessAttemptExists(reservationId: Long): Boolean
|
fun isSuccessAttemptExists(reservationId: Long): Boolean
|
||||||
|
|
||||||
|
fun findAllByReservationId(reservationId: Long): List<PaymentAttemptEntity>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -108,6 +108,8 @@ class OrderConcurrencyTest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
delay(10)
|
||||||
|
|
||||||
async {
|
async {
|
||||||
assertThrows<OrderException> {
|
assertThrows<OrderException> {
|
||||||
orderService.confirm(reservation.id, paymentConfirmRequest)
|
orderService.confirm(reservation.id, paymentConfirmRequest)
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export function maxIterations() {
|
|||||||
|
|
||||||
export function fetchUsers() {
|
export function fetchUsers() {
|
||||||
const userCount = Math.round(maxIterations() * 0.5)
|
const userCount = Math.round(maxIterations() * 0.5)
|
||||||
const userAccountRes = http.get(`${BASE_URL}/tests/users?count=${userCount}`)
|
const userAccountRes = http.get(`http://localhost:8080/tests/users?count=${userCount}`)
|
||||||
|
|
||||||
if (userAccountRes.status !== 200) {
|
if (userAccountRes.status !== 200) {
|
||||||
throw new Error('users 조회 실패')
|
throw new Error('users 조회 실패')
|
||||||
@ -42,7 +42,7 @@ export function fetchUsers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function fetchStores() {
|
export function fetchStores() {
|
||||||
const storeIdListRes = http.get(`${BASE_URL}/tests/stores`)
|
const storeIdListRes = http.get(`http://localhost:8080/tests/stores`)
|
||||||
|
|
||||||
if (storeIdListRes.status !== 200) {
|
if (storeIdListRes.status !== 200) {
|
||||||
throw new Error('stores 조회 실패')
|
throw new Error('stores 조회 실패')
|
||||||
|
|||||||
@ -15,19 +15,10 @@ export const options = {
|
|||||||
scenarios: {
|
scenarios: {
|
||||||
user_reservation: {
|
user_reservation: {
|
||||||
executor: 'ramping-vus',
|
executor: 'ramping-vus',
|
||||||
startVUs: 0,
|
startVUs: 1500,
|
||||||
stages: [
|
stages: [
|
||||||
{ duration: '1m', target: 100 },
|
{ duration: '10m', target: 1500 },
|
||||||
{ duration: '1m', target: 200 },
|
{ duration: '1m', target: 0 }
|
||||||
{ duration: '1m', target: 300 },
|
|
||||||
{ duration: '1m', target: 300 },
|
|
||||||
{ duration: '1m', target: 400 },
|
|
||||||
{ duration: '1m', target: 500 },
|
|
||||||
{ duration: '2m', target: 500 },
|
|
||||||
{ duration: '1m', target: 600 },
|
|
||||||
{ duration: '1m', target: 800 },
|
|
||||||
{ duration: '1m', target: 1000 },
|
|
||||||
{ duration: '3m', target: 0 },
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -84,13 +75,13 @@ export default function (data) {
|
|||||||
|
|
||||||
const user = randomItem(users)
|
const user = randomItem(users)
|
||||||
const accessToken = login(user.account, user.password, 'USER').accessToken
|
const accessToken = login(user.account, user.password, 'USER').accessToken
|
||||||
const storeId = randomItem(stores).storeId
|
let storeId = randomItem(stores).storeId
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
console.log(`로그인 실패: token=${accessToken}`)
|
console.log(`로그인 실패: token=${accessToken}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const targetDate = randomDayBetween(1, 4)
|
let targetDate
|
||||||
|
|
||||||
let availableScheduleId, selectedThemeId, selectedThemeInfo, reservationId, totalAmount
|
let availableScheduleId, selectedThemeId, selectedThemeInfo, reservationId, totalAmount
|
||||||
|
|
||||||
@ -99,6 +90,8 @@ export default function (data) {
|
|||||||
let schedules
|
let schedules
|
||||||
|
|
||||||
while (searchTrial < 5) {
|
while (searchTrial < 5) {
|
||||||
|
storeId = randomItem(stores).storeId
|
||||||
|
targetDate = randomDayBetween(1, 7)
|
||||||
const res = http.get(`${BASE_URL}/stores/${storeId}/schedules?date=${targetDate}`)
|
const res = http.get(`${BASE_URL}/stores/${storeId}/schedules?date=${targetDate}`)
|
||||||
const result = check(res, {'일정 조회 성공': (r) => r.status === 200})
|
const result = check(res, {'일정 조회 성공': (r) => r.status === 200})
|
||||||
if (result !== true) {
|
if (result !== true) {
|
||||||
@ -145,7 +138,7 @@ export default function (data) {
|
|||||||
let isScheduleHeld = false
|
let isScheduleHeld = false
|
||||||
group(`일정 Holding 및 테마 정보 조회 -> 예약 과정중 첫 페이지의 작업 완료`, function () {
|
group(`일정 Holding 및 테마 정보 조회 -> 예약 과정중 첫 페이지의 작업 완료`, function () {
|
||||||
const holdRes = http.post(`${BASE_URL}/schedules/${availableScheduleId}/hold`, null, getHeaders(accessToken))
|
const holdRes = http.post(`${BASE_URL}/schedules/${availableScheduleId}/hold`, null, getHeaders(accessToken))
|
||||||
const body = JSON.parse(holdRes)
|
const body = JSON.parse(holdRes.body)
|
||||||
|
|
||||||
if (check(holdRes, {'일정 점유 성공': (r) => r.status === 200})) {
|
if (check(holdRes, {'일정 점유 성공': (r) => r.status === 200})) {
|
||||||
const themeInfoRes = http.get(`${BASE_URL}/themes/${selectedThemeId}`)
|
const themeInfoRes = http.get(`${BASE_URL}/themes/${selectedThemeId}`)
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export const options = {
|
|||||||
executor: 'shared-iterations',
|
executor: 'shared-iterations',
|
||||||
vus: 263,
|
vus: 263,
|
||||||
iterations: TOTAL_ITERATIONS,
|
iterations: TOTAL_ITERATIONS,
|
||||||
maxDuration: '10m',
|
maxDuration: '30m',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
thresholds: {
|
thresholds: {
|
||||||
@ -113,7 +113,7 @@ function createSchedule(storeId, accessToken, schedule) {
|
|||||||
function generateDates(days) {
|
function generateDates(days) {
|
||||||
const dates = [];
|
const dates = [];
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
for (let i = 1; i < days; i++) {
|
for (let i = 1; i <= days; i++) {
|
||||||
const date = new Date(today);
|
const date = new Date(today);
|
||||||
date.setDate(today.getDate() + i);
|
date.setDate(today.getDate() + i);
|
||||||
dates.push(date.toISOString().split('T')[0]);
|
dates.push(date.toISOString().split('T')[0]);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user