925 lines
37 KiB
Kotlin

package com.sangdol.data
import com.sangdol.common.persistence.IDGenerator
import com.sangdol.common.persistence.TransactionExecutionUtil
import com.sangdol.common.utils.KoreaDateTime
import com.sangdol.roomescape.admin.infrastructure.persistence.AdminEntity
import com.sangdol.roomescape.admin.infrastructure.persistence.AdminPermissionLevel
import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType
import com.sangdol.roomescape.payment.business.domain.BankCode
import com.sangdol.roomescape.payment.business.domain.CardIssuerCode
import com.sangdol.roomescape.payment.business.domain.CardOwnerType
import com.sangdol.roomescape.payment.business.domain.CardType
import com.sangdol.roomescape.payment.business.domain.EasyPayCompanyCode
import com.sangdol.roomescape.payment.business.domain.PaymentMethod
import com.sangdol.roomescape.payment.business.domain.PaymentStatus
import com.sangdol.roomescape.payment.business.domain.PaymentType
import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationStatus
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
import com.sangdol.roomescape.store.infrastructure.persistence.StoreEntity
import com.sangdol.roomescape.supports.AdminFixture
import com.sangdol.roomescape.supports.FunSpecSpringbootTest
import com.sangdol.roomescape.supports.randomPhoneNumber
import com.sangdol.roomescape.supports.randomString
import com.sangdol.roomescape.theme.infrastructure.persistence.Difficulty
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity
import com.sangdol.roomescape.user.business.SIGNUP
import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity
import com.sangdol.roomescape.user.infrastructure.persistence.UserStatus
import com.sangdol.roomescape.user.dto.UserContactResponse
import io.kotest.core.test.TestCaseOrder
import jakarta.persistence.EntityManager
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Semaphore
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.test.context.ActiveProfiles
import java.sql.Timestamp
import java.time.Instant
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZoneId
@ActiveProfiles("test", "data")
abstract class AbstractDataInitializer(
val semaphore: Semaphore = Semaphore(permits = 10),
) : FunSpecSpringbootTest(
enableCleanerExtension = false
) {
@Autowired
lateinit var entityManager: EntityManager
@Autowired
lateinit var jdbcTemplate: JdbcTemplate
@Autowired
lateinit var transactionExecutionUtil: TransactionExecutionUtil
@Autowired
lateinit var idGenerator: IDGenerator
override fun testCaseOrder(): TestCaseOrder? = TestCaseOrder.Sequential
suspend fun initialize() {
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
jdbcTemplate.execute("SET FOREIGN_KEY_CHECKS = 0")
jdbcTemplate.query("SHOW TABLES") { rs, _ ->
rs.getString(1).lowercase()
}.forEach {
jdbcTemplate.execute("TRUNCATE TABLE $it")
}
jdbcTemplate.execute("SET FOREIGN_KEY_CHECKS = 1")
this::class.java.getResource("/schema/region-data.sql")?.readText()?.let { sql ->
jdbcTemplate.execute(sql)
}
}
}
suspend fun executeBatch(sql: String, batchArgs: List<Array<Any>>) {
semaphore.acquire()
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
jdbcTemplate.batchUpdate(sql, batchArgs)
}
semaphore.release()
}
}
class DefaultDataInitializer : AbstractDataInitializer() {
// 1. HQ Admin 추가
// 2. Store 추가 -> CreatedBy / UpdatedBy는 HQ Admin 중 한명으로 고정
// 3. Store Admin 추가 -> CreatedBy / UpdatedBy는 HQ Admin 본인으로 고정
init {
lateinit var superHQAdmin: AdminEntity
// 모든 테이블 초기화 + 지역 데이터 삽입 + HQ 관리자 1명 생성
beforeSpec {
initialize()
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
superHQAdmin = testAuthUtil.createAdmin(AdminFixture.hqDefault.apply {
this.createdBy = this.id
this.updatedBy = this.id
})
}
}
context("관리자, 매장, 테마 초기 데이터 생성") {
test("각 PermissionLevel 마다 20명의 HQ 관리자 생성") {
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
AdminPermissionLevel.entries.forEach {
repeat(20) { index ->
AdminFixture.create(
account = "hq_${it.name.lowercase()}_$index",
name = randomKoreanName(),
phone = randomPhoneNumber(),
type = AdminType.HQ,
permissionLevel = it
).apply {
this.createdBy = superHQAdmin.id
this.updatedBy = superHQAdmin.id
}.also {
entityManager.persist(it)
}
}
}
}
}
test("전체 매장 생성") {
val storeDataInitializer = StoreDataInitializer()
val creatableAdminIds: List<String> = transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
entityManager.createQuery(
"SELECT a.id FROM AdminEntity a WHERE a.type = :type AND a.permissionLevel IN (:permissionLevels)",
Long::class.java
).setParameter("type", AdminType.HQ)
.setParameter(
"permissionLevels",
listOf(AdminPermissionLevel.FULL_ACCESS, AdminPermissionLevel.WRITABLE)
)
.resultList
}!!.map { it.toString() }
val sqlFile = storeDataInitializer.createStoreDataSqlFile(creatableAdminIds)
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
jdbcTemplate.execute(sqlFile.readText())
}
}
test("각 매장당 1명의 ${AdminPermissionLevel.FULL_ACCESS} 권한의 StoreManager 1명 + ${AdminPermissionLevel.WRITABLE}의 2명 + 나머지 권한은 3명씩 생성") {
val storeAdminCountsByPermissionLevel = mapOf(
AdminPermissionLevel.WRITABLE to 2,
AdminPermissionLevel.READ_ALL to 3,
AdminPermissionLevel.READ_SUMMARY to 3
)
val stores: List<StoreEntity> = transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
entityManager.createQuery(
"SELECT s FROM StoreEntity s",
StoreEntity::class.java
).resultList
}!!
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
stores.forEach { store ->
// StoreManager 1명 생성
val storeManager = AdminFixture.create(
account = store.name,
name = randomKoreanName(),
phone = randomPhoneNumber(),
type = AdminType.STORE,
storeId = store.id,
permissionLevel = AdminPermissionLevel.FULL_ACCESS
).apply {
this.createdBy = superHQAdmin.id
this.updatedBy = superHQAdmin.id
}.also {
entityManager.persist(it)
}
storeAdminCountsByPermissionLevel.forEach { (permissionLevel, count) ->
repeat(count) { index ->
AdminFixture.create(
account = "${store.name}-${permissionLevel.ordinal}${index}",
name = randomKoreanName(),
phone = randomPhoneNumber(),
type = AdminType.STORE,
storeId = store.id,
permissionLevel = permissionLevel
).apply {
this.createdBy = storeManager.id
this.updatedBy = storeManager.id
}.also {
entityManager.persist(it)
}
}
}
entityManager.flush()
entityManager.clear()
}
}
}
test("총 500개의 테마 생성: 지난 2년 전 부터 지금까지의 랜덤 테마 + active 상태인 1달 이내 생성 테마 10개") {
val creatableAdminIds: List<Long> = transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
entityManager.createQuery(
"SELECT a.id FROM AdminEntity a WHERE a.type = :type AND a.permissionLevel IN (:permissionLevels)",
Long::class.java
).setParameter("type", AdminType.HQ)
.setParameter(
"permissionLevels",
listOf(AdminPermissionLevel.FULL_ACCESS, AdminPermissionLevel.WRITABLE)
)
.resultList
}!!
val sql =
"INSERT INTO theme (id, name, description, thumbnail_url, is_active, available_minutes, expected_minutes_from, expected_minutes_to, price, difficulty, min_participants, max_participants, created_at, created_by, updated_at, updated_by) VALUES (?," +
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
val batchSize = 100
val batchArgs = mutableListOf<Array<Any>>()
repeat(500) { i ->
val randomDay = if (i <= 9) (7..30).random() else (30..365 * 2).random()
val randomCreatedAt: LocalDateTime = LocalDateTime.now().minusDays(randomDay.toLong())
val randomThemeName =
(1..7).random().let { repeat -> (1..repeat).joinToString("") { randomKoreanName() } }
val availableMinutes = (6..20).random() * 10
val expectedMinutesTo = availableMinutes - ((1..3).random() * 10)
val expectedMinutesFrom = expectedMinutesTo - ((1..2).random() * 10)
val randomPrice = (0..40).random() * 500
val minParticipant = (1..10).random()
val maxParticipant = minParticipant + (1..10).random()
val createdBy = creatableAdminIds.random()
batchArgs.add(
arrayOf(
idGenerator.create(),
randomThemeName,
"$randomThemeName 설명이에요!!",
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRFiuCdwdz88l6pdfsRy1nFl0IHUVI7JMTQHg&s",
if (randomDay <= 30) true else false,
availableMinutes.toShort(),
expectedMinutesFrom.toShort(),
expectedMinutesTo.toShort(),
randomPrice,
Difficulty.entries.random().name,
minParticipant.toShort(),
maxParticipant.toShort(),
Timestamp.valueOf(randomCreatedAt),
createdBy,
Timestamp.valueOf(randomCreatedAt),
createdBy
)
)
}
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
jdbcTemplate.batchUpdate(sql, batchArgs)
}
}
}
}
}
class UserDataInitializer : AbstractDataInitializer() {
val userCount = 1_000_000
init {
context("유저 초기 데이터 생성") {
test("$userCount 명의 회원 생성") {
val regions: List<String> = transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
entityManager.createQuery(
"SELECT r.code FROM RegionEntity r",
String::class.java
).resultList
}!!
val chunkSize = 10_000
val chunks = userCount / chunkSize
coroutineScope {
(1..chunks).map {
launch(Dispatchers.IO) {
processUsers(chunkSize, regions)
}
}.joinAll()
}
}
test("휴대폰 번호가 중복된 유저들에게 재배정") {
val duplicatePhoneUsers: List<UserEntity> =
transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
entityManager.createQuery(
"""
SELECT u FROM UserEntity u
WHERE u.phone IN (
SELECT u2.phone FROM UserEntity u2
GROUP BY u2.phone
HAVING COUNT(u2.id) > 1
)
ORDER BY u.phone, u.id
""".trimIndent(),
UserEntity::class.java
).resultList
}!!
jdbcTemplate.execute("CREATE INDEX idx_users__phone ON users (phone)")
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
var currentPhone: String? = null
duplicatePhoneUsers.forEach { user ->
if (user.phone != currentPhone) {
currentPhone = user.phone
} else {
var newPhone: String
do {
newPhone = randomPhoneNumber()
val count: Long = entityManager.createQuery(
"SELECT COUNT(u.id) FROM UserEntity u WHERE u.phone = :phone",
Long::class.java
).setParameter("phone", newPhone)
.singleResult
if (count == 0L) break
} while (true)
user.phone = newPhone
user.updatedAt = Instant.now()
entityManager.merge(user)
}
}
}
jdbcTemplate.execute("DROP INDEX idx_users__phone ON users")
}
test("회원 상태 변경 이력 저장") {
val userId: List<Long> = transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
entityManager.createQuery(
"SELECT u.id FROM UserEntity u",
Long::class.java
).resultList
}!!
coroutineScope {
userId.chunked(10_000).map { chunk ->
launch(Dispatchers.IO) {
processStatus(chunk)
}
}.joinAll()
}
}
}
}
private suspend fun processStatus(userIds: List<Long>) {
val sql = """
INSERT INTO user_status_history (
id, user_id, reason, status,
created_by, updated_by, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""".trimIndent()
val batchArgs = mutableListOf<Array<Any>>()
val now = LocalDateTime.now()
userIds.forEach { userId ->
batchArgs.add(
arrayOf(
idGenerator.create(),
userId,
SIGNUP,
UserStatus.ACTIVE.name,
userId,
userId,
Timestamp.valueOf(now),
Timestamp.valueOf(now)
)
)
}
executeBatch(sql, batchArgs).also { batchArgs.clear() }
}
private suspend fun processUsers(chunkSize: Int, regions: List<String>) {
val sql = """
INSERT INTO users (
id, name, email, password, phone, region_code, status,
created_by, updated_by, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""".trimIndent()
val batchArgs = mutableListOf<Array<Any>>()
repeat(chunkSize) {
val id: Long = idGenerator.create()
batchArgs.add(
arrayOf(
id,
randomKoreanName(),
"${randomString()}@sangdol.com",
randomString(),
randomPhoneNumber(),
regions.random(),
UserStatus.ACTIVE.name,
id,
id,
Timestamp.valueOf(LocalDateTime.now()),
Timestamp.valueOf(LocalDateTime.now())
)
)
if (batchArgs.size >= 1_000) {
executeBatch(sql, batchArgs).also { batchArgs.clear() }
}
}
if (batchArgs.isNotEmpty()) executeBatch(sql, batchArgs).also { batchArgs.clear() }
}
}
class ScheduleDataInitializer : AbstractDataInitializer() {
init {
context("일정 초기 데이터 생성") {
test("테마 생성일 기준으로 다음 3일차, 매일 최대 10개의 일정을 모든 매장에 생성") {
val stores: List<Pair<Long, Long>> = getStoreWithManagers()
val themes: List<ThemeEntity> = getThemes()
val maxScheduleCountPerDay = 10
val startTime = LocalTime.of(10, 0)
val themeWithTimes: Map<ThemeEntity, List<LocalTime>> = themes.associateWith { theme ->
val times = mutableListOf<LocalTime>()
val themeAvailableMinutes = theme.availableMinutes
var lastTime = startTime
while (times.size <= maxScheduleCountPerDay && lastTime.hour in (10..23)) {
times.add(lastTime)
lastTime = lastTime.plusMinutes(themeAvailableMinutes + 10L)
}
times
}
coroutineScope {
stores.map { store ->
launch(Dispatchers.IO) {
processTheme(store, themeWithTimes)
}
}
}
}
test("내일 ~ 일주일 뒤 까지의 일정 생성") {
// val stores: List<Pair<Long, Long>> = getStoreWithManagers()
// val availableThemes: List<ThemeEntity> = transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
// entityManager.createQuery(
// "SELECT t FROM ThemeEntity t WHERE t.isActive = true AND t.createdAt >", ThemeEntity::class.java
// ).resultList
// }!!.take(10)
coroutineScope {
val jobs = (1..100).map { i ->
launch(Dispatchers.IO) {
val threadName = Thread.currentThread().name
println("[$i] 시작: $threadName")
delay(1)
println("[$i] 완료: $threadName")
}
}
}
}
}
}
private suspend fun processTheme(
store: Pair<Long, Long>,
themeWithTimes: Map<ThemeEntity, List<LocalTime>>
) {
val sql = """
INSERT INTO schedule (
id, store_id, theme_id, date, time, status,
created_by, updated_by, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""".trimIndent()
val batchArgs = mutableListOf<Array<Any>>()
val status = ScheduleStatus.RESERVED.name
themeWithTimes.forEach { (theme, times) ->
val themeCreatedAt = theme.createdAt
(1..3).forEach {
val themeCreatedDateTime = themeCreatedAt.atZone(ZoneId.systemDefault())
val themeCreatedDate = themeCreatedDateTime.toLocalDate().plusDays(it.toLong())
val themeCreatedTime = themeCreatedDateTime.toLocalTime()
times.forEach { time ->
val storeId = store.first
val storeAdminId = store.second
batchArgs.add(
arrayOf(
idGenerator.create(),
storeId,
theme.id,
themeCreatedDate,
time,
status,
storeAdminId,
storeAdminId,
themeCreatedTime.plusHours(1),
themeCreatedTime.plusHours(1)
)
)
if (batchArgs.size >= 300) {
executeBatch(sql, batchArgs).also { batchArgs.clear() }
}
}
}
}
if (batchArgs.isNotEmpty()) {
executeBatch(sql, batchArgs).also { batchArgs.clear() }
}
}
private fun getStoreWithManagers(): List<Pair<Long, Long>> {
return transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
entityManager.createQuery(
"SELECT a.storeId, a.id FROM AdminEntity a WHERE a.type = :type AND a.permissionLevel = :permissionLevel",
List::class.java
).setParameter("type", AdminType.STORE)
.setParameter("permissionLevel", AdminPermissionLevel.FULL_ACCESS)
.resultList
}!!.map {
val array = it as List<*>
Pair(array[0] as Long, array[1] as Long)
}
}
private fun getThemes(): List<ThemeEntity> {
return transactionExecutionUtil.withNewTransaction(isReadOnly = true) {
entityManager.createQuery(
"SELECT t FROM ThemeEntity t",
ThemeEntity::class.java
).resultList
}!!
}
}
/**
* 아래의 ReservationDataInitializer 에서 사용할 임시 DTO 클래스
*/
data class ScheduleWithThemeParticipants(
val scheduleId: Long,
val themeMinParticipants: Short,
val themeMaxParticipants: Short,
)
class ReservationDataInitializer : AbstractDataInitializer() {
init {
context("예약 초기 데이터 생성") {
test("${ScheduleStatus.RESERVED}인 모든 일정에 예약을 1개씩 배정한다.") {
val chunkSize = 500
val chunkedSchedules: List<List<ScheduleWithThemeParticipants>> = entityManager.createQuery(
"SELECT new com.sangdol.roomescape.data.ScheduleWithThemeParticipants(s._id, t.minParticipants, t.maxParticipants) FROM ScheduleEntity s JOIN ThemeEntity t ON s.themeId = t.id WHERE s.status = :status",
ScheduleWithThemeParticipants::class.java
).setParameter("status", ScheduleStatus.RESERVED).resultList.chunked(chunkSize)
val chunkedUsers: List<List<UserContactResponse>> = entityManager.createQuery(
"SELECT new com.sangdol.roomescape.user.web.UserContactResponse(u._id, u.name, u.phone) FROM UserEntity u",
UserContactResponse::class.java
).resultList.chunked(chunkSize)
coroutineScope {
chunkedSchedules.forEachIndexed { idx, schedules ->
launch(Dispatchers.IO) {
processReservation(chunkedUsers[idx % chunkedUsers.size], schedules)
}
}
}
}
}
}
private suspend fun processReservation(
users: List<UserContactResponse>,
schedules: List<ScheduleWithThemeParticipants>
) {
val sql = """
INSERT INTO reservation (
id, user_id, schedule_id,
reserver_name, reserver_contact, participant_count, requirement,
status, created_at, created_by, updated_at, updated_by
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""".trimIndent()
val batchArgs = mutableListOf<Array<Any>>()
val createdAt = LocalDateTime.now()
schedules.forEachIndexed { idx, schedule ->
val user: UserContactResponse = users[idx % users.size]
batchArgs.add(
arrayOf(
idGenerator.create(),
user.id,
schedule.scheduleId,
user.name,
user.phone,
(schedule.themeMinParticipants..schedule.themeMaxParticipants).random(),
randomKoreanWords(length = (20..100).random()),
ReservationStatus.CONFIRMED.name,
Timestamp.valueOf(createdAt),
user.id,
Timestamp.valueOf(createdAt),
user.id,
)
)
}
if (batchArgs.isNotEmpty()) executeBatch(sql, batchArgs).also { batchArgs.clear() }
}
}
class ReservationWithPrice(
themePrice: Int,
participantCount: Short,
val reservationId: Long,
val totalPrice: Int = (themePrice * participantCount),
)
data class PaymentWithMethods(
val id: Long,
val totalPrice: Int,
val method: PaymentMethod
)
class PaymentDataInitializer : AbstractDataInitializer() {
companion object {
val requestedAtCache: Timestamp = Timestamp.valueOf(KoreaDateTime.nowWithOffset().toLocalDateTime())
val approvedAtCache: Timestamp =
Timestamp.valueOf(KoreaDateTime.nowWithOffset().plusSeconds(5).toLocalDateTime())
val supportedPaymentMethods = listOf(PaymentMethod.TRANSFER, PaymentMethod.EASY_PAY, PaymentMethod.CARD)
val supportedCardType = listOf(CardType.CREDIT, CardType.CHECK)
val settlementStatus = "COMPLETED"
val paymentSql: String = """
INSERT INTO payment(
id, reservation_id, type, method,
payment_key, order_id, total_amount, status,
requested_at, approved_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""".trimIndent()
val paymentDetailSql: String = """
INSERT INTO payment_detail(
id, payment_id, supplied_amount, vat
) VALUES (?, ?, ?, ?)
""".trimIndent()
val paymentCardDetailSql: String = """
INSERT INTO payment_card_detail(
id, issuer_code, card_type, owner_type,
amount, card_number, approval_number, installment_plan_months,
is_interest_free, easypay_provider_code, easypay_discount_amount
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""".trimIndent()
val paymentEasypayPrepaidDetailSql: String = """
INSERT INTO payment_easypay_prepaid_detail(
id, easypay_provider_code, amount, discount_amount
) VALUES (?, ?, ?, ?)
""".trimIndent()
val paymentBankTransferDetailSql: String = """
INSERT INTO payment_bank_transfer_detail(
id, bank_code, settlement_status
) VALUES (?, ?, ?)
""".trimIndent()
}
init {
context("결제 데이터 초기화") {
test("모든 예약에 맞춰 1:1로 결제 및 결제 상세 데이터를 생성한다.") {
val allReservations: List<ReservationWithPrice> = entityManager.createQuery(
"SELECT t.price, r.participantCount, r._id FROM ReservationEntity r JOIN ScheduleEntity s ON s._id = r.scheduleId JOIN ThemeEntity t ON t.id = s.themeId",
List::class.java
).resultList.map {
val items = it as List<*>
ReservationWithPrice(
themePrice = items[0] as Int,
participantCount = items[1] as Short,
reservationId = items[2] as Long
)
}
coroutineScope {
allReservations.chunked(500).forEach { reservations ->
launch(Dispatchers.IO) {
processPaymentAndDefaultDetail(reservations)
}
}
}
}
test("기존 결제 데이터에 상세 정보(계좌이체, 카드, 간편결제) 데이터를 생성한다.") {
val allPayments: List<PaymentWithMethods> = entityManager.createQuery(
"SELECT new com.sangdol.roomescape.data.PaymentWithMethods(pd._id, p.totalAmount, p.method) FROM PaymentEntity p JOIN PaymentDetailEntity pd ON p._id = pd.paymentId",
PaymentWithMethods::class.java
).resultList
coroutineScope {
allPayments.chunked(500).forEach { payments ->
launch(Dispatchers.IO) {
processPaymentDetail(payments)
}
}
}
}
test("null 컴파일 에러를 피하기 위해 문자열 null로 임시 지정한 컬럼을 변경한다.") {
jdbcTemplate.execute("CREATE INDEX idx_payment_card_detail_easypay ON payment_card_detail (easypay_provider_code)")
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
jdbcTemplate.update(
"UPDATE payment_card_detail SET easypay_provider_code = ? WHERE easypay_provider_code = ?",
null,
"null"
)
}
jdbcTemplate.execute("DROP INDEX idx_payment_card_detail_easypay ON payment_card_detail")
}
}
}
private suspend fun processPaymentAndDefaultDetail(reservations: List<ReservationWithPrice>) {
val paymentBatchArgs = mutableListOf<Array<Any>>()
val detailBatchArgs = mutableListOf<Array<Any>>()
reservations.forEachIndexed { idx, reservations ->
val id = idGenerator.create()
val totalPrice = reservations.totalPrice
paymentBatchArgs.add(
arrayOf(
id,
reservations.reservationId,
PaymentType.NORMAL.name,
randomPaymentMethod(),
randomString(length = 64),
randomString(length = 20),
totalPrice,
PaymentStatus.DONE.name,
requestedAtCache,
approvedAtCache,
)
)
val suppliedAmount: Int = (totalPrice * 0.9).toInt()
val vat: Int = (totalPrice - suppliedAmount)
detailBatchArgs.add(
arrayOf(
idGenerator.create(),
id,
suppliedAmount,
vat
)
)
}
if (paymentBatchArgs.isNotEmpty()) {
executeBatch(paymentSql, paymentBatchArgs).also { paymentBatchArgs.clear() }
}
if (detailBatchArgs.isNotEmpty()) {
executeBatch(paymentDetailSql, detailBatchArgs).also { detailBatchArgs.clear() }
}
}
private suspend fun processPaymentDetail(payments: List<PaymentWithMethods>) {
val transferBatchArgs = mutableListOf<Array<Any>>()
val cardBatchArgs = mutableListOf<Array<Any>>()
val easypayPrepaidBatchArgs = mutableListOf<Array<Any>>()
payments.forEach { payment ->
val totalPrice = payment.totalPrice
val randomDiscountAmount =
if (totalPrice < 100 || Math.random() < 0.8) 0 else ((100..totalPrice).random() / 100) * 100
val amount = totalPrice - randomDiscountAmount
when (payment.method) {
PaymentMethod.TRANSFER -> {
transferBatchArgs.add(
arrayOf(
payment.id,
BankCode.entries.random().name,
settlementStatus
)
)
}
PaymentMethod.EASY_PAY -> {
if (Math.random() <= 0.7) {
cardBatchArgs.add(
arrayOf(
payment.id,
CardIssuerCode.entries.random().name,
supportedCardType.random().name,
CardOwnerType.PERSONAL.name,
amount,
randomCardNumber(),
randomApprovalNumber(),
randomInstallmentPlanMonths(amount),
true,
EasyPayCompanyCode.entries.random().name,
randomDiscountAmount
)
)
} else {
easypayPrepaidBatchArgs.add(
arrayOf(
payment.id,
EasyPayCompanyCode.entries.random().name,
amount,
randomDiscountAmount,
)
)
}
}
PaymentMethod.CARD -> {
cardBatchArgs.add(
arrayOf(
payment.id,
CardIssuerCode.entries.random().name,
supportedCardType.random().name,
CardOwnerType.PERSONAL.name,
totalPrice,
randomCardNumber(),
randomApprovalNumber(),
randomInstallmentPlanMonths(totalPrice),
true,
"null",
0,
)
)
}
else -> return@forEach
}
}
if (transferBatchArgs.isNotEmpty()) {
executeBatch(paymentBankTransferDetailSql, transferBatchArgs).also { transferBatchArgs.clear() }
}
if (cardBatchArgs.isNotEmpty()) {
executeBatch(paymentCardDetailSql, cardBatchArgs).also { cardBatchArgs.clear() }
}
if (easypayPrepaidBatchArgs.isNotEmpty()) {
executeBatch(
paymentEasypayPrepaidDetailSql,
easypayPrepaidBatchArgs
).also { easypayPrepaidBatchArgs.clear() }
}
}
private suspend fun randomPaymentMethod(): String {
val random = Math.random()
return if (random <= 0.5) {
PaymentMethod.EASY_PAY.name
} else if (random <= 0.9) {
PaymentMethod.CARD.name
} else {
PaymentMethod.TRANSFER.name
}
}
private suspend fun randomCardNumber(): String {
return "${(10000000..99999999).random()}****${(100..999).random()}*"
}
private suspend fun randomApprovalNumber(): String {
return "${(10000000..99999999).random()}"
}
private suspend fun randomInstallmentPlanMonths(amount: Int): Int {
return if (amount < 50_000 || Math.random() < 0.9) {
0
} else {
(1..6).random()
}
}
}
fun randomKoreanName(): String {
val lastNames = listOf(
"", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""
)
return "${lastNames.random()}${if (Math.random() < 0.1) randomKoreanWords(1) else randomKoreanWords(2)}"
}
fun randomKoreanWords(length: Int = 1): String {
val words = listOf(
"", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "",
"", "", "", "", "",
)
return (1..length).joinToString("") { words.random() }
}