Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import umc.th.juinjang.api.pencil.controller.request.AppleIAPPurchaseRequest;
import umc.th.juinjang.api.pencil.service.PencilCommandService;
import umc.th.juinjang.api.pencil.service.PencilQueryService;
import umc.th.juinjang.api.pencil.service.response.AcquiredPencilReadResponse;
import umc.th.juinjang.api.pencil.service.response.AcquiredPencilReadStatusResponse;
import umc.th.juinjang.api.pencil.service.response.AcquiredPencilResponse;
import umc.th.juinjang.api.pencil.service.response.AppleIAPPurchaseResponse;
import umc.th.juinjang.api.pencil.service.response.PurchasedPencilResponse;
Expand All @@ -40,10 +42,21 @@ public ApiResponse<List<AcquiredPencilResponse>> getAcquiredPencilHistory(@Authe

@Operation(summary = "얻은 연필 목록에서 읽음 처리를 진행한다.")
@PatchMapping("/acquired/{acquiredPencilId}/read")
public ApiResponse<Boolean> markAcquiredPencilAsRead(
public ApiResponse<AcquiredPencilReadResponse> markAcquiredPencilAsRead(
@PathVariable Long acquiredPencilId,
@AuthenticationPrincipal Member member) {
return ApiResponse.onSuccess(pencilCommandService.markAcquiredPencilAsRead(acquiredPencilId));
return ApiResponse.onSuccess(
AcquiredPencilReadResponse.of(pencilCommandService.markAcquiredPencilAsRead(acquiredPencilId),
pencilQueryService.isAcquiredPencilReadStatus(member)));
}

@Operation(summary = "얻은 연필 목록에서 읽지 않은 항목이 존재하는 여부를 확인한다.")
@GetMapping("/acquired/is-total-read")
public ApiResponse<AcquiredPencilReadStatusResponse> isAcquiredPencilReadStatus(
@AuthenticationPrincipal Member member
) {
return ApiResponse.onSuccess(
AcquiredPencilReadStatusResponse.of(pencilQueryService.isAcquiredPencilReadStatus(member)));
}

@Operation(summary = "구매한 연필 목록을 불러온다")
Expand All @@ -66,6 +79,7 @@ public ApiResponse<AppleIAPPurchaseResponse> purchasePencil(
@AuthenticationPrincipal Member member,
@RequestBody AppleIAPPurchaseRequest request
) {
return ApiResponse.onSuccess(pencilCommandService.processAppleIAPPurchase(request , member, LocalDateTime.now()));
return ApiResponse.onSuccess(
pencilCommandService.processAppleIAPPurchase(request, member, LocalDateTime.now()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ public List<AcquiredPencil> findAllByMemberOrderByCreatedAtDesc(Member member) {
return acquiredPencilRepository.findAllByMemberOrderByCreatedAtDesc(member);
}

public boolean existsByMemberAndIsReadFalse(Member member) {
return acquiredPencilRepository.existsByMemberAndIsReadFalse(member);
}

public AcquiredPencil findById(Long id) {
return acquiredPencilRepository.findById(id).orElse(null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import umc.th.juinjang.domain.pencil.acquired.model.AcquiredPencil;
import umc.th.juinjang.domain.pencil.purchased.model.PurchasedPencil;
import umc.th.juinjang.domain.pencil.purchased.model.TransactionStatus;
import umc.th.juinjang.domain.pencilaccount.model.PencilAccount;

@Slf4j
@Service
Expand Down Expand Up @@ -48,7 +49,7 @@ public Boolean markAcquiredPencilAsRead(Long acquiredPencilId) {
public AppleIAPPurchaseResponse processAppleIAPPurchase(AppleIAPPurchaseRequest request, Member member,
LocalDateTime now) {
String transactionId = request.getTransactionId();

Long purchaseQuantity = request.getPencilQuantity();
Optional<PurchasedPencil> existing = purchasedPencilFinder.findByTransactionIdAndMember(transactionId, member);

if (existing.isEmpty()) {
Expand All @@ -59,32 +60,36 @@ public AppleIAPPurchaseResponse processAppleIAPPurchase(AppleIAPPurchaseRequest

PurchasedPencil pencil = existing.get();
TransactionStatus status = pencil.getTransactionStatus();
PencilAccount buyer = pencilAccountFinder.findByMember(member);

if (status == TransactionStatus.SUCCESS) {
// 트랜잭션이 정상적으로 성공된 기록이 있는 경우
return AppleIAPPurchaseResponse.ofSuccess(transactionId);
return AppleIAPPurchaseResponse.ofSuccess(transactionId, purchaseQuantity, buyer.getTotalBalance());
}

PurchasedPencil newPencil = retryPurchasedPencil(pencil, member); // 실패 재시도 처리
return AppleIAPPurchaseResponse.of(transactionId, newPencil.getTransactionStatus());
return AppleIAPPurchaseResponse.of(transactionId, newPencil.getTransactionStatus(), purchaseQuantity,
buyer.getTotalBalance());
}



@Transactional
public AppleIAPPurchaseResponse validateAndCommitApplePurchase(AppleIAPPurchaseRequest request, Member member, LocalDateTime now) {
public AppleIAPPurchaseResponse validateAndCommitApplePurchase(AppleIAPPurchaseRequest request, Member member,
LocalDateTime now) {
String transactionId = request.getTransactionId();

VerificationResult verificationResult = appleService.verifyAppleTransaction(AppleTransactionVerifyCommand.fromRequest(request));
VerificationResult verificationResult = appleService.verifyAppleTransaction(
AppleTransactionVerifyCommand.fromRequest(request));

if (VerificationResult.isSuccess(verificationResult)){
// 성공 시, DB에 저장z
if (VerificationResult.isSuccess(verificationResult)) {
// 성공 시, DB에 저장
handleSuccessfulApplePurchase(request, member, now);

// TODO : 디스코드 알림 추가 필요
// paymentEventPublisher.publishPaymentEvent(member,request.getPrice(), pencilAmount,TransactionStatus.SUCCESS);
return AppleIAPPurchaseResponse.ofSuccess(transactionId);
}else{
PencilAccount buyer = pencilAccountFinder.findByMember(member);
return AppleIAPPurchaseResponse.ofSuccess(transactionId, request.getPencilQuantity(),
buyer.getTotalBalance());
} else {
// 실패 시, DB에 저장
handleFailureApplePurchase(request, member, now);

Expand Down Expand Up @@ -112,12 +117,13 @@ public void handleFailureApplePurchase(AppleIAPPurchaseRequest request, Member m
Long pencilAmount = request.getPencilQuantity();

String title = createTitle(pencilAmount);
purchasedPencilUpdater.save(PurchasedPencil.failedDueToValidation(member, title, pencilAmount, request.getPrice(),
request.getPlayTime(), transactionId, request.getAppAccountToken(), now));
purchasedPencilUpdater.save(
PurchasedPencil.failedDueToValidation(member, title, pencilAmount, request.getPrice(),
request.getPlayTime(), transactionId, request.getAppAccountToken(), now));
}

private PurchasedPencil retryPurchasedPencil(PurchasedPencil pencil, Member member) {
if ( pencil.getRetryCount() >= 3 ) { // 재시도 횟수가 3회 이상일 경우 실패로 처리
if (pencil.getRetryCount() >= 3) { // 재시도 횟수가 3회 이상일 경우 실패로 처리
return pencil;
}

Expand All @@ -137,7 +143,6 @@ private PurchasedPencil retryPurchasedPencil(PurchasedPencil pencil, Member memb
return pencil;
}


private String createTitle(Long pencilAmount) {
return String.format("연필 %d개 구매", pencilAmount);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,9 @@ public List<UsedPencilResponse> getUsedPencils(Member member) {
.map(UsedPencilResponse::from)
.toList();
}

public boolean isAcquiredPencilReadStatus(Member member) {
// false 인 것이 존재하면 안됨.
return !acquiredPencilFinder.existsByMemberAndIsReadFalse(member);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package umc.th.juinjang.api.pencil.service.response;

public record AcquiredPencilReadResponse(
boolean isMarked,
boolean isTotalRead
) {
public static AcquiredPencilReadResponse of(boolean isMarked, boolean isTotalRead) {
return new AcquiredPencilReadResponse(isMarked, isTotalRead);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package umc.th.juinjang.api.pencil.service.response;

public record AcquiredPencilReadStatusResponse(
boolean isTotalRead
) {
public static AcquiredPencilReadStatusResponse of(boolean isTotalRead) {
return new AcquiredPencilReadStatusResponse(isTotalRead);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,58 @@
@Getter
public class AppleIAPPurchaseResponse {

private TransactionStatus status;
private String transactionId;
private final TransactionStatus status;
private final String transactionId;
private final Long purchaseQuantity;
private final Long remainQuantity;

@Builder
private AppleIAPPurchaseResponse(TransactionStatus status,String transactionId) {
private AppleIAPPurchaseResponse(
TransactionStatus status,
String transactionId,
Long purchaseQuantity,
Long remainQuantity
) {
this.status = status;
this.transactionId = transactionId;
this.purchaseQuantity = purchaseQuantity;
this.remainQuantity = remainQuantity;
}

public static AppleIAPPurchaseResponse of(String transactionId, TransactionStatus status) {
/**
* 일반적인 정적 팩토리: 모든 필드 수동 지정
*/
public static AppleIAPPurchaseResponse of(String transactionId, TransactionStatus status, Long purchaseQuantity,
Long remainQuantity) {
return AppleIAPPurchaseResponse.builder()
.transactionId(transactionId)
.status(status)
.purchaseQuantity(purchaseQuantity)
.remainQuantity(remainQuantity)
.build();
}

public static AppleIAPPurchaseResponse ofSuccess(String transactionId) {
/**
* 성공 응답용 팩토리
*/
public static AppleIAPPurchaseResponse ofSuccess(String transactionId, Long purchaseQuantity, Long remainQuantity) {
return AppleIAPPurchaseResponse.builder()
.status(TransactionStatus.SUCCESS)
.transactionId(transactionId)
.purchaseQuantity(purchaseQuantity)
.remainQuantity(remainQuantity)
.build();
}

/**
* 검증 실패 응답용 팩토리
*/
public static AppleIAPPurchaseResponse ofValidationFailure(String transactionId) {
return AppleIAPPurchaseResponse.builder()
.status(TransactionStatus.VALIDATION_FAILED)
.transactionId(transactionId)
.purchaseQuantity(0L)
.remainQuantity(null)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@

import static umc.th.juinjang.common.code.status.ErrorStatus.*;

import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Component;

import jakarta.persistence.LockModeType;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import umc.th.juinjang.common.exception.handler.PencilAccountHandler;
Expand All @@ -21,7 +18,7 @@ public class PencilAccountFinder {

private final PencilAccountRepository pencilAccountRepository;

protected PencilAccount findByMember(Member member) {
public PencilAccount findByMember(Member member) {
return pencilAccountRepository.findByMember(member).orElseThrow(
() -> {
log.error("[PENCIL_ACCOUNT]");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@

public interface AcquiredPencilRepository extends JpaRepository<AcquiredPencil, Long> {
List<AcquiredPencil> findAllByMemberOrderByCreatedAtDesc(Member member);

boolean existsByMemberAndIsReadFalse(Member member);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package umc.th.juinjang.api.pencil.service;

import static org.assertj.core.api.Assertions.*;
import static umc.th.juinjang.domain.pencil.purchased.model.PurchasedPencil.*;

import java.time.LocalDateTime;
import java.util.List;
Expand Down Expand Up @@ -156,11 +155,16 @@ void getPurchasedPencilsOrderedByCreatedAtDesc() {
UUID uuid5 = UUID.randomUUID();

// 명확한 순서로 데이터 생성 (시간 역순으로)
PurchasedPencil pencil1 = PurchasedPencil.successOf(member, "10개 연필팩", 10L, 1000L, 0L,"transaction1", uuid1, time1);
PurchasedPencil pencil2 = PurchasedPencil.successOf(member, "20개 연필팩", 20L, 2000L, 0L,"transaction2", uuid2, time2);
PurchasedPencil pencil3 = PurchasedPencil.successOf(member, "30개 연필팩", 30L, 3000L,0L ,"transaction3", uuid3, time3);
PurchasedPencil pencil4 = PurchasedPencil.successOf(member, "15개 연필팩", 15L, 1500L, 0L,"transaction4", uuid4, time4);
PurchasedPencil pencil5 = PurchasedPencil.successOf(member, "25개 연필팩", 25L, 2500L, 0L,"transaction5", uuid5, time5);
PurchasedPencil pencil1 = PurchasedPencil.successOf(member, "10개 연필팩", 10L, 1000L, 0L, "transaction1", uuid1,
time1);
PurchasedPencil pencil2 = PurchasedPencil.successOf(member, "20개 연필팩", 20L, 2000L, 0L, "transaction2", uuid2,
time2);
PurchasedPencil pencil3 = PurchasedPencil.successOf(member, "30개 연필팩", 30L, 3000L, 0L, "transaction3", uuid3,
time3);
PurchasedPencil pencil4 = PurchasedPencil.successOf(member, "15개 연필팩", 15L, 1500L, 0L, "transaction4", uuid4,
time4);
PurchasedPencil pencil5 = PurchasedPencil.successOf(member, "25개 연필팩", 25L, 2500L, 0L, "transaction5", uuid5,
time5);

purchasedPencilRepository.saveAll(List.of(pencil1, pencil2, pencil3, pencil4, pencil5));

Expand Down Expand Up @@ -193,7 +197,8 @@ void getPurchasedPencilsWithoutDeliveryStatus() {
LocalDateTime time = LocalDateTime.now();
UUID uuid = UUID.randomUUID();

PurchasedPencil pencil = PurchasedPencil.failedDueToServerError(member, "10개 연필팩", 10L, 1000L, 10L,"transaction1", uuid, time);
PurchasedPencil pencil = PurchasedPencil.failedDueToServerError(member, "10개 연필팩", 10L, 1000L, 10L,
"transaction1", uuid, time);

purchasedPencilRepository.saveAll(List.of(pencil));

Expand Down Expand Up @@ -239,6 +244,42 @@ void getUsedPencilsOrderedByCreatedAtDesc() {
);
}

@DisplayName("얻은 연필 목록 중 읽지 않은 연필이 없으면 전체 읽음으로 판단된다.")
@Test
void returnTrueIfAllAcquiredPencilsAreRead() {
// given
Member member = MemberFixture.createDefaultMember();
memberRepository.save(member);

AcquiredPencil pencil1 = AcquiredPencil.create(member, "연필 1", 1L, 10L, true, AcquiredType.NOTE);
AcquiredPencil pencil2 = AcquiredPencil.create(member, "연필 2", 2L, 20L, true, AcquiredType.SOLD);
acquiredPencilRepository.saveAll(List.of(pencil1, pencil2));

// when
boolean isTotalRead = pencilService.isAcquiredPencilReadStatus(member);

// then
assertThat(isTotalRead).isTrue();
}

@DisplayName("얻은 연필 목록 중 읽지 않은 연필이 하나라도 있으면 전체 읽음으로 판단되지 않는다.")
@Test
void returnFalseIfAnyAcquiredPencilIsUnread() {
// given
Member member = MemberFixture.createDefaultMember();
memberRepository.save(member);

AcquiredPencil pencil1 = AcquiredPencil.create(member, "연필 1", 1L, 10L, true, AcquiredType.NOTE);
AcquiredPencil pencil2 = AcquiredPencil.create(member, "연필 2", 2L, 20L, false, AcquiredType.SOLD);
acquiredPencilRepository.saveAll(List.of(pencil1, pencil2));

// when
boolean isTotalRead = pencilService.isAcquiredPencilReadStatus(member);

// then
assertThat(isTotalRead).isFalse();
}

private AcquiredPencil createAcquiredPencilWithTime(LocalDateTime createdAt, String content, Long sharedNoteId,
Long acquiredQuantity, boolean isRead, AcquiredType type, Member member) {
return AcquiredPencil.createWithDate(member, content, sharedNoteId, acquiredQuantity, isRead, type, createdAt);
Expand Down