From 373b11348bcc691ee37df852a6573ce5e168c32c Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 1 Nov 2026 14:33:35 +0900 Subject: [PATCH] =?UTF-8?q?feat(gift):=20=EC=A0=84=EB=8B=AC=EB=B6=88?= =?UTF-8?q?=EA=B0=80=20=EC=84=A0=EB=AC=BC=EC=9D=98=20=EB=B0=B0=EC=86=A1=20?= =?UTF-8?q?=EA=B0=9C=EC=9D=B8=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=20=ED=9B=84=20=ED=8C=8C=EA=B8=B0=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/out/persistence/GiftRepository.kt | 27 ++-- .../adapter/out/scheduler/GiftScheduler.kt | 12 +- .../adapter/in/web/AdminGiftControllerTest.kt | 52 +++++--- .../out/scheduler/GiftSchedulerTest.kt | 116 +++++++++++------- 4 files changed, 129 insertions(+), 78 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftRepository.kt index e18c12cd..f2d83e92 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftRepository.kt @@ -108,17 +108,26 @@ interface GiftRepository : JpaRepository, GiftLockRepository { """ select g from Gift g - where g.status = kr.co.vividnext.sodalive.v2.gift.domain.GiftStatus.DELIVERED - and exists ( - select d.id - from GiftDelivery d - where d.giftId = g.id - and d.deliveredAt is not null - and d.deliveredAt <= :cutoff - ) + where exists ( + select d.id + from GiftDelivery d + where d.giftId = g.id + and ( + ( + g.status = kr.co.vividnext.sodalive.v2.gift.domain.GiftStatus.DELIVERED + and d.deliveredAt is not null + and d.deliveredAt <= :cutoff + ) + or ( + g.status = kr.co.vividnext.sodalive.v2.gift.domain.GiftStatus.UNDELIVERABLE + and d.undeliverableAt is not null + and d.undeliverableAt <= :cutoff + ) + ) + ) """ ) - fun findDeliveredGiftsWithDeliveryBeforeOrAt(@Param("cutoff") cutoff: LocalDateTime): List + fun findGiftsWithPersonalDataRetentionExpired(@Param("cutoff") cutoff: LocalDateTime): List @Query( """ diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftScheduler.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftScheduler.kt index 1f7e2960..94b1bb65 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftScheduler.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftScheduler.kt @@ -92,23 +92,23 @@ class GiftScheduler( @Scheduled(cron = "0 20 * * * *", zone = "Asia/Seoul") @Transactional - fun runDeliveredGiftPersonalDataErasure() { - val lock = redissonClient.getLock(DELIVERED_GIFT_PERSONAL_DATA_ERASURE_LOCK_KEY) + fun runGiftPersonalDataErasure() { + val lock = redissonClient.getLock(GIFT_PERSONAL_DATA_ERASURE_LOCK_KEY) if (!lock.tryLock(0, -1, TimeUnit.SECONDS)) { return } try { - eraseDeliveredGiftPersonalData(LocalDateTime.now()) + eraseGiftPersonalData(LocalDateTime.now()) } finally { unlockAfterTransaction(lock) } } @Transactional - fun eraseDeliveredGiftPersonalData(now: LocalDateTime): Int { + fun eraseGiftPersonalData(now: LocalDateTime): Int { val cutoff = now.minusDays(7) - val expiredGifts = giftRepository.findDeliveredGiftsWithDeliveryBeforeOrAt(cutoff) + val expiredGifts = giftRepository.findGiftsWithPersonalDataRetentionExpired(cutoff) return expiredGifts.count { gift -> val delivery = requireNotNull(giftDeliveryRepository.findByGiftId(requireNotNull(gift.id))) @@ -218,7 +218,7 @@ class GiftScheduler( const val DEADLINE_REMINDER_HOURS = 24L const val EXPIRED_TRACKING_REGISTRATION_GIFTS_LOCK_KEY = "lock:gift-expired-tracking-registration-cancellation" const val EXPIRED_RECIPIENT_ADDRESS_GIFTS_LOCK_KEY = "lock:gift-expired-recipient-address-undeliverable" - const val DELIVERED_GIFT_PERSONAL_DATA_ERASURE_LOCK_KEY = "lock:gift-delivered-personal-data-erasure" + const val GIFT_PERSONAL_DATA_ERASURE_LOCK_KEY = "lock:gift-delivered-personal-data-erasure" const val DEADLINE_REMINDER_GIFTS_LOCK_KEY = "lock:gift-deadline-reminders" const val EXPIRED_RECIPIENT_ADDRESS_REASON = "배송지 미입력 기한 초과" } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt index 8768e9d1..6aabf491 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt @@ -162,7 +162,7 @@ class AdminGiftControllerTest @Autowired constructor( mapOf( "runExpiredTrackingRegistrationGiftCancellation" to "0 0 * * * *", "runExpiredRecipientAddressGiftExpiration" to "0 10 * * * *", - "runDeliveredGiftPersonalDataErasure" to "0 20 * * * *", + "runGiftPersonalDataErasure" to "0 20 * * * *", "runDeadlineReminderGifts" to "0 40 * * * *" ), scheduledJobs.associate { @@ -311,12 +311,13 @@ class AdminGiftControllerTest @Autowired constructor( .andExpect(jsonPath("$.data.availableActions[1]").value("MARK_UNDELIVERABLE")) } - @Test - @DisplayName("관리자는 개인정보가 지워진 전달 완료 선물을 기존 빈 문자열과 null 응답으로 조회한다") - fun shouldReturnCompatibleErasedAdminGiftResponses() { + @ParameterizedTest + @EnumSource(value = GiftStatus::class, names = ["DELIVERED", "UNDELIVERABLE"]) + @DisplayName("관리자는 개인정보가 지워진 종료 선물을 기존 빈 문자열과 null 응답으로 조회한다") + fun shouldReturnCompatibleErasedAdminGiftResponses(giftStatus: GiftStatus) { val (gift, delivery) = saveAdminGift( applicationNo = "A-ERASED-CONTROLLER", - status = GiftStatus.DELIVERED, + status = giftStatus, senderNickname = "erased-admin-fan", recipientNickname = "erased-admin-creator", withTracking = false @@ -331,7 +332,7 @@ class AdminGiftControllerTest @Autowired constructor( mockMvc.perform(get("/api/v2/admin/gifts/{applicationNo}", gift.applicationNo).with(user("admin").roles("ADMIN"))) .andExpect(status().isOk) .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data.status").value("DELIVERED")) + .andExpect(jsonPath("$.data.status").value(giftStatus.name)) .andExpect(jsonPath("$.data.senderInfo.nickname").value("erased-admin-fan")) .andExpect(jsonPath("$.data.senderInfo.name").value("")) .andExpect(jsonPath("$.data.senderInfo.phoneNumber").value("")) @@ -359,9 +360,10 @@ class AdminGiftControllerTest @Autowired constructor( .andExpect(jsonPath("$.data.items[0].availableActions").isEmpty) } - @Test + @ParameterizedTest + @EnumSource(value = GiftStatus::class, names = ["DELIVERED", "UNDELIVERABLE"]) @DisplayName("실제 Spring 개인정보 파기는 JWT HTTP 선물함 계약과 결제·동의·리뷰·이력을 보존한다") - fun shouldEraseDeliveredGiftAndPreserveRealJwtHttpContracts() { + fun shouldEraseGiftAndPreserveRealJwtHttpContracts(status: GiftStatus) { val now = NOW.plusDays(14) val recipient = memberRepository.saveAndFlush( Member(email = "${UUID.randomUUID()}@test.com", password = "password", nickname = "erasure-recipient") @@ -376,7 +378,7 @@ class AdminGiftControllerTest @Autowired constructor( val review = requireNotNull( transaction.execute { val gift = giftRepository.findById(giftId).orElseThrow() - gift.status = GiftStatus.DELIVERED + gift.status = status gift.recipientTermsAgreed = true gift.recipientPrivacyAgreed = true gift.recipientTermsAgreedAt = NOW @@ -393,7 +395,9 @@ class AdminGiftControllerTest @Autowired constructor( delivery.trackingRegisteredAt = NOW.minusDays(2) delivery.arrivedAtMailboxAt = NOW.minusDays(1) delivery.inspectionCompletedAt = NOW.plusDays(1) - delivery.deliveredAt = now.minusDays(7) + delivery.deliveredAt = now.minusDays(7).takeIf { status == GiftStatus.DELIVERED } + delivery.undeliverableAt = now.minusDays(7).takeIf { status == GiftStatus.UNDELIVERABLE } + delivery.undeliverableReason = "검수 실패".takeIf { status == GiftStatus.UNDELIVERABLE } delivery.recipientAddressDeadlineAt = NOW.plusDays(5) delivery.trackingDeadlineReminderSentAt = NOW.minusDays(3) delivery.recipientAddressDeadlineReminderSentAt = NOW.plusDays(4) @@ -437,8 +441,8 @@ class AdminGiftControllerTest @Autowired constructor( listOf(false, true).forEach { erased -> if (erased) { assertFalse(TransactionSynchronizationManager.isActualTransactionActive()) - assertEquals(1, scheduler.eraseDeliveredGiftPersonalData(now)) - assertEquals(0, scheduler.eraseDeliveredGiftPersonalData(now)) + assertEquals(1, scheduler.eraseGiftPersonalData(now)) + assertEquals(0, scheduler.eraseGiftPersonalData(now)) assertFalse(TransactionSynchronizationManager.isActualTransactionActive()) } listOf(detailPath, adminPath, adminListPath, "/api/v2/gifts?type=SENT", "/api/v2/gifts?type=RECEIVED").forEach { @@ -476,16 +480,27 @@ class AdminGiftControllerTest @Autowired constructor( originalTimeline = originalTimeline ?: sentData.path("statusTimeline") assertTrue(requireNotNull(originalTimeline).isArray) assertEquals(5, requireNotNull(originalTimeline).size()) - assertEquals(json.textNode("2026-10-10T12:00:00Z"), requireNotNull(originalTimeline).last().path("occurredAt")) + assertEquals( + if (status == GiftStatus.DELIVERED) json.textNode("2026-10-10T12:00:00Z") else json.nullNode(), + requireNotNull(originalTimeline).last().path("occurredAt") + ) listOf(sentData, receivedData).forEach { data -> assertEquals(originalTimeline, data.path("statusTimeline")) + assertEquals( + if (status == GiftStatus.UNDELIVERABLE) json.textNode("2026-10-10T12:00:00Z") else json.nullNode(), + data.path("delivery").path("undeliverableAt") + ) + assertEquals( + if (status == GiftStatus.UNDELIVERABLE) json.textNode("검수 실패") else json.nullNode(), + data.path("delivery").path("undeliverableReason") + ) assertEquals(json.booleanNode(false), data.path("trackingRequired")) assertEquals(json.booleanNode(false), data.path("recipientAddressRequired")) assertEquals(json.nullNode(), data.path("mailbox")) } listOf(sentData, receivedData, adminData).forEach { data -> assertEquals(json.textNode(APPLICATION_NO), data.path("applicationNo")) - assertEquals(json.textNode("DELIVERED"), data.path("status")) + assertEquals(json.textNode(status.name), data.path("status")) } listOf("senderInfo" to senderAddress, "recipientInfo" to recipientAddress).forEach { (field, address) -> val nickname = if (field == "senderInfo") fixture.sender.nickname else recipient.nickname @@ -499,7 +514,7 @@ class AdminGiftControllerTest @Autowired constructor( assertEquals(json.numberNode(1), adminList.path("data").path("totalCount")) val adminItem = adminList.path("data").path("items").single() assertEquals(json.textNode(APPLICATION_NO), adminItem.path("applicationNo")) - assertEquals(json.textNode("DELIVERED"), adminItem.path("status")) + assertEquals(json.textNode(status.name), adminItem.path("status")) assertEquals(json.textNode(fixture.sender.nickname), adminItem.path("senderNickname")) assertEquals(json.textNode(recipient.nickname), adminItem.path("recipientNickname")) listOf(adminData.path("inboundDeliveryInfo"), adminItem).forEach { data -> @@ -514,13 +529,13 @@ class AdminGiftControllerTest @Autowired constructor( assertEquals(json.numberNode(1), list.path("data").path("totalCount")) val item = list.path("data").path("items").single() assertEquals(json.textNode(APPLICATION_NO), item.path("applicationNo")) - assertEquals(json.textNode("DELIVERED"), item.path("status")) + assertEquals(json.textNode(status.name), item.path("status")) assertEquals(json.textNode(direction), item.path("direction")) assertEquals(json.textNode(nickname), item.path("counterpartNickname")) assertEquals(json.booleanNode(!erased), item.path("recipientAddressRegistered")) } println( - "P15_HTTP erased=$erased status=200 success=true " + + "P17_HTTP giftStatus=$status erased=$erased status=200 success=true " + "senderStringsEmpty=${sentData.path("senderInfo").all { it.isTextual && it.textValue().isEmpty() }} " + "recipientAddressNull=${receivedData.path("recipientAddress").isNull} " + "trackingNull=${sentData.path("giftInfo").path("tracking").isNull} " + @@ -549,7 +564,8 @@ class AdminGiftControllerTest @Autowired constructor( .refundGiftCan(requireNotNull(fixture.sender.id), fixture.gift.canUsageId) Mockito.verifyNoInteractions(giftPushService) println( - "P15_DB erased=1 repeated=0 persistedNulls=12 giftRows=1 deliveryRows=1 paymentConsentReviewHistoryPreserved=true" + "P17_DB giftStatus=$status erased=1 repeated=0 persistedNulls=12 giftRows=1 deliveryRows=1 " + + "paymentConsentReviewHistoryPreserved=true" ) } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftSchedulerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftSchedulerTest.kt index 4278a082..d96f8bfa 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftSchedulerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftSchedulerTest.kt @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource import org.junit.jupiter.params.provider.MethodSource import org.mockito.Mockito import org.redisson.api.RLock @@ -608,43 +609,51 @@ class GiftSchedulerTest @Autowired constructor( Mockito.verifyNoInteractions(giftPushService) } - @Test - @DisplayName("전달 완료 후 정확히 7일인 선물의 배송 개인정보 12개를 DB에서 지운다") - fun shouldEraseDeliveredGiftWhenDeliveredAtEqualsSevenDayCutoff() { - val (gift, _) = saveGiftForErasure(ERASURE_CUTOFF) + @ParameterizedTest + @EnumSource(value = GiftStatus::class, names = ["DELIVERED", "UNDELIVERABLE"]) + @DisplayName("상태와 짝인 시각부터 정확히 7일인 선물의 배송 개인정보 12개를 DB에서 지운다") + fun shouldEraseGiftWhenMatchingTimestampEqualsSevenDayCutoff(status: GiftStatus) { + val (gift, _) = saveGiftForErasure(ERASURE_CUTOFF, status) flushAndClear() - val eligibleGifts = giftRepository.findDeliveredGiftsWithDeliveryBeforeOrAt(ERASURE_CUTOFF) + assertEquals(1, scheduler.eraseGiftPersonalData(ERASURE_NOW)) + val eligibleGifts = giftRepository.findGiftsWithPersonalDataRetentionExpired(ERASURE_CUTOFF) assertEquals(listOf(gift.id), eligibleGifts.map { it.id }) - assertEquals(1, scheduler.eraseDeliveredGiftPersonalData(ERASURE_NOW)) flushAndClear() assertEquals(List(12) { null }, personalData(reloadDelivery(gift))) } - @Test - @DisplayName("전달 완료 후 7일보다 1초 덜 지난 선물의 개인정보는 보존한다") - fun shouldIgnoreDeliveredGiftOneSecondNewerThanSevenDayCutoff() { - val (gift, delivery) = saveGiftForErasure(ERASURE_CUTOFF.plusSeconds(1)) + @ParameterizedTest + @EnumSource(value = GiftStatus::class, names = ["DELIVERED", "UNDELIVERABLE"]) + @DisplayName("짝인 시각부터 7일보다 1초 덜 지났으면 다른 시각이 오래돼도 개인정보를 보존한다") + fun shouldIgnoreGiftOneSecondNewerThanSevenDayCutoffDespiteOldOtherTimestamp(status: GiftStatus) { + val (gift, delivery) = saveGiftForErasure(ERASURE_CUTOFF.plusSeconds(1), status) + if (status == GiftStatus.DELIVERED) { + delivery.undeliverableAt = ERASURE_CUTOFF.minusDays(30) + } else { + delivery.deliveredAt = ERASURE_CUTOFF.minusDays(30) + } val originalPersonalData = personalData(delivery) flushAndClear() - assertEquals(0, scheduler.eraseDeliveredGiftPersonalData(ERASURE_NOW)) + assertEquals(0, scheduler.eraseGiftPersonalData(ERASURE_NOW)) flushAndClear() assertEquals(originalPersonalData, personalData(reloadDelivery(gift))) } - @Test - @DisplayName("도입 시점보다 오래전에 생성되고 전달 완료된 과거 누적 선물도 지운다") - fun shouldEraseOlderHistoricalDeliveredGifts() { + @ParameterizedTest + @EnumSource(value = GiftStatus::class, names = ["DELIVERED", "UNDELIVERABLE"]) + @DisplayName("도입 시점보다 오래전에 생성되고 종료된 과거 누적 선물도 지운다") + fun shouldEraseOlderHistoricalGifts(status: GiftStatus) { val historicalAt = LocalDateTime.of(2020, 1, 1, 12, 0) - val (gift, delivery) = saveGiftForErasure(historicalAt.plusDays(10)) + val (gift, delivery) = saveGiftForErasure(historicalAt.plusDays(10), status) gift.createdAt = historicalAt delivery.createdAt = historicalAt flushAndClear() - assertEquals(1, scheduler.eraseDeliveredGiftPersonalData(ERASURE_NOW)) + assertEquals(1, scheduler.eraseGiftPersonalData(ERASURE_NOW)) flushAndClear() assertEquals(List(12) { null }, personalData(reloadDelivery(gift))) @@ -653,17 +662,29 @@ class GiftSchedulerTest @Autowired constructor( } @Test - @DisplayName("완료 시각이 없거나 전달 완료가 아닌 모든 상태는 개인정보를 보존한다") - fun shouldIgnoreNullDeliveredAtAndNonDeliveredGifts() { - val gifts = listOf(saveGiftForErasure(null)) + GiftStatus.values() - .filter { it != GiftStatus.DELIVERED } + @DisplayName("짝인 시각이 없거나 지원하지 않는 상태는 다른 시각이 오래돼도 개인정보를 보존한다") + fun shouldIgnoreNullMatchingTimestampsAndUnsupportedStatuses() { + val supportedStatuses = setOf(GiftStatus.DELIVERED, GiftStatus.UNDELIVERABLE) + val gifts = supportedStatuses.map { status -> + saveGiftForErasure(null, status, "NULL-$status").also { (_, delivery) -> + if (status == GiftStatus.DELIVERED) { + delivery.undeliverableAt = ERASURE_CUTOFF.minusDays(30) + } else { + delivery.deliveredAt = ERASURE_CUTOFF.minusDays(30) + } + } + } + GiftStatus.values() + .filter { it !in supportedStatuses } .mapIndexed { index, status -> - saveGiftForErasure(ERASURE_CUTOFF.minusDays(30), status, "IGNORE-$index") + saveGiftForErasure(null, status, "IGNORE-$index").also { (_, delivery) -> + delivery.deliveredAt = ERASURE_CUTOFF.minusDays(30) + delivery.undeliverableAt = ERASURE_CUTOFF.minusDays(30) + } } val originals = gifts.map { (_, delivery) -> personalData(delivery) } flushAndClear() - assertEquals(0, scheduler.eraseDeliveredGiftPersonalData(ERASURE_NOW)) + assertEquals(0, scheduler.eraseGiftPersonalData(ERASURE_NOW)) flushAndClear() gifts.forEachIndexed { index, (gift, _) -> @@ -672,56 +693,60 @@ class GiftSchedulerTest @Autowired constructor( } } - @Test + @ParameterizedTest + @EnumSource(value = GiftStatus::class, names = ["DELIVERED", "UNDELIVERABLE"]) @DisplayName("12개 개인정보 중 어느 하나만 남아 있어도 나머지 값을 DB에서 지운다") - fun shouldEraseRemainingFieldsFromPartiallyClearedGift() { + fun shouldEraseRemainingFieldsFromPartiallyClearedGift(status: GiftStatus) { val gifts = PERSONAL_DATA_FIELDS.mapIndexed { index, remainingField -> - saveGiftForErasure(ERASURE_CUTOFF, applicationNo = "PARTIAL-$index").also { (_, delivery) -> + saveGiftForErasure(ERASURE_CUTOFF, status, "PARTIAL-$index").also { (_, delivery) -> PERSONAL_DATA_FIELDS.filter { it != remainingField }.forEach { it.set(delivery, null) } } } flushAndClear() - assertEquals(12, scheduler.eraseDeliveredGiftPersonalData(ERASURE_NOW)) + assertEquals(12, scheduler.eraseGiftPersonalData(ERASURE_NOW)) flushAndClear() gifts.forEach { (gift, _) -> assertEquals(List(12) { null }, personalData(reloadDelivery(gift))) } } - @Test + @ParameterizedTest + @EnumSource(value = GiftStatus::class, names = ["DELIVERED", "UNDELIVERABLE"]) @DisplayName("이미 개인정보가 모두 null인 선물은 첫 실행부터 변경하지 않는다") - fun shouldReturnZeroWhenAllPersonalDataIsAlreadyNull() { - val (gift, delivery) = saveGiftForErasure(ERASURE_CUTOFF) + fun shouldReturnZeroWhenAllPersonalDataIsAlreadyNull(status: GiftStatus) { + val (gift, delivery) = saveGiftForErasure(ERASURE_CUTOFF, status) PERSONAL_DATA_FIELDS.forEach { it.set(delivery, null) } flushAndClear() val updatedAt = reloadDelivery(gift).updatedAt - assertEquals(0, scheduler.eraseDeliveredGiftPersonalData(ERASURE_NOW)) + assertEquals(0, scheduler.eraseGiftPersonalData(ERASURE_NOW)) flushAndClear() assertEquals(List(12) { null }, personalData(reloadDelivery(gift))) assertEquals(updatedAt, reloadDelivery(gift).updatedAt) } - @Test + @ParameterizedTest + @EnumSource(value = GiftStatus::class, names = ["DELIVERED", "UNDELIVERABLE"]) @DisplayName("개인정보 정리를 재실행하면 추가 변경 없이 0건을 반환한다") - fun shouldReturnZeroWhenErasureRunsAgain() { - val (gift, _) = saveGiftForErasure(ERASURE_CUTOFF) + fun shouldReturnZeroWhenErasureRunsAgain(status: GiftStatus) { + val (gift, _) = saveGiftForErasure(ERASURE_CUTOFF, status) flushAndClear() - assertEquals(1, scheduler.eraseDeliveredGiftPersonalData(ERASURE_NOW)) + assertEquals(1, scheduler.eraseGiftPersonalData(ERASURE_NOW)) flushAndClear() val updatedAt = reloadDelivery(gift).updatedAt - assertEquals(0, scheduler.eraseDeliveredGiftPersonalData(ERASURE_NOW.plusDays(1))) + assertEquals(0, scheduler.eraseGiftPersonalData(ERASURE_NOW.plusDays(1))) flushAndClear() assertEquals(List(12) { null }, personalData(reloadDelivery(gift))) assertEquals(updatedAt, reloadDelivery(gift).updatedAt) } - @Test + @ParameterizedTest + @EnumSource(value = GiftStatus::class, names = ["DELIVERED", "UNDELIVERABLE"]) @DisplayName("개인정보 정리는 선물·회원·결제·동의·리뷰와 업무 시각을 DB에 보존하고 환불·푸시하지 않는다") - fun shouldPreserveGiftMemberPaymentConsentReviewAndBusinessTimestamps() { + fun shouldPreserveGiftMemberPaymentConsentReviewAndBusinessTimestamps(status: GiftStatus) { val sender = entityManager.persist( Member(password = "test-password", nickname = "sender", profileImage = "sender-profile.png") .apply { pgChargeCan = 120 } @@ -733,7 +758,7 @@ class GiftSchedulerTest @Autowired constructor( UseCan(canUsage = CanUsage.GIFT, can = 60, rewardCan = 20).apply { member = sender } ) val (gift, delivery) = saveGift( - GiftStatus.DELIVERED, + status, APPLIED_AT.plusDays(3), senderMemberId = requireNotNull(sender.id), recipientMemberId = requireNotNull(recipient.id), @@ -752,7 +777,7 @@ class GiftSchedulerTest @Autowired constructor( delivery.inspectionCompletedAt = APPLIED_AT.plusDays(4) delivery.deliveredAt = ERASURE_CUTOFF delivery.canceledAt = APPLIED_AT.plusHours(1) - delivery.undeliverableAt = APPLIED_AT.plusHours(2) + delivery.undeliverableAt = if (status == GiftStatus.UNDELIVERABLE) ERASURE_CUTOFF else APPLIED_AT.plusHours(2) delivery.undeliverableReason = "보존할 업무 사유" val review = entityManager.persist( GiftReview(requireNotNull(gift.id), requireNotNull(sender.id), 5, "[\"안전\"]", "감사합니다") @@ -764,7 +789,7 @@ class GiftSchedulerTest @Autowired constructor( val originalReview = entityManager.find(GiftReview::class.java, review.id) flushAndClear() - assertEquals(1, scheduler.eraseDeliveredGiftPersonalData(ERASURE_NOW)) + assertEquals(1, scheduler.eraseGiftPersonalData(ERASURE_NOW)) flushAndClear() val reloadedGift = entityManager.find(Gift::class.java, gift.id) @@ -849,13 +874,14 @@ class GiftSchedulerTest @Autowired constructor( private fun personalData(delivery: GiftDelivery): List = PERSONAL_DATA_FIELDS.map { it.get(delivery) } private fun saveGiftForErasure( - deliveredAt: LocalDateTime?, + retentionStartedAt: LocalDateTime?, status: GiftStatus = GiftStatus.DELIVERED, applicationNo: String = APPLICATION_NO ): Pair = saveGift(status, APPLIED_AT.plusDays(3), applicationNo = applicationNo) .also { (_, delivery) -> fillDeliveryPersonalData(delivery) - delivery.deliveredAt = deliveredAt + delivery.deliveredAt = retentionStartedAt.takeIf { status == GiftStatus.DELIVERED } + delivery.undeliverableAt = retentionStartedAt.takeIf { status == GiftStatus.UNDELIVERABLE } } private fun fillDeliveryPersonalData(delivery: GiftDelivery) { @@ -928,11 +954,11 @@ class GiftSchedulerTest @Autowired constructor( GiftScheduler::runExpiredRecipientAddressGiftExpiration ), ScheduledEntryPoint( - "runDeliveredGiftPersonalDataErasure", - "eraseDeliveredGiftPersonalData", + "runGiftPersonalDataErasure", + "eraseGiftPersonalData", "0 20 * * * *", "lock:gift-delivered-personal-data-erasure", - GiftScheduler::runDeliveredGiftPersonalDataErasure + GiftScheduler::runGiftPersonalDataErasure ), ScheduledEntryPoint( "runDeadlineReminderGifts",