Skip to content

Commit

Permalink
YEL-117 [develop] apple 결제 검증 배포
Browse files Browse the repository at this point in the history
YEL-117 [develop] apple 결제 검증 배포
  • Loading branch information
hyeonjeongs authored Aug 17, 2023
2 parents e285a48 + f74ecbc commit 20473b8
Show file tree
Hide file tree
Showing 18 changed files with 322 additions and 134 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ out/
### config ###
application.yml
application-dev.yml
application-apple.yml
firebase*.json

### monitoring ###
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import static com.yello.server.global.common.SuccessCode.USER_SUBSCRIBE_NEEDED_READ_SUCCESS;
import static com.yello.server.global.common.SuccessCode.VERIFY_RECEIPT_SUCCESS;

import com.yello.server.domain.purchase.dto.apple.AppleVerifyReceipt;
import com.yello.server.domain.purchase.dto.apple.AppleVerifyReceiptResponse;
import com.yello.server.domain.purchase.dto.apple.AppleOrderResponse;
import com.yello.server.domain.purchase.dto.apple.AppleTransaction;
import com.yello.server.domain.purchase.dto.response.UserSubscribeNeededResponse;
import com.yello.server.domain.purchase.service.PurchaseService;
import com.yello.server.domain.user.entity.User;
Expand Down Expand Up @@ -32,14 +32,36 @@ public class PurchaseController {

private final PurchaseService purchaseService;

@PostMapping("/verify")
public BaseResponse<AppleVerifyReceiptResponse> verifyReceipt(
@RequestBody AppleVerifyReceipt appleVerifyReceipt,
@Operation(summary = "Apple 구독 구매 검증 API", responses = {
@ApiResponse(
responseCode = "200",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = AppleOrderResponse.class))
)
})
@PostMapping("/apple/verify/subscribe")
public BaseResponse verifyAppleSubscriptionTransaction(
@RequestBody AppleTransaction appleTransaction,
@AccessTokenUser User user
) {
purchaseService.verifyAppleSubscriptionTransaction(user.getId(), appleTransaction);

return BaseResponse.success(VERIFY_RECEIPT_SUCCESS);
}

@Operation(summary = "Apple 열람권 구매 검증 API", responses = {
@ApiResponse(
responseCode = "200",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = AppleOrderResponse.class))
)
})
@PostMapping("/apple/verify/ticket")
public BaseResponse verifyAppleTicketTransaction(
@RequestBody AppleTransaction appleTransaction,
@AccessTokenUser User user
) {
val data = purchaseService.verifyReceipt(user.getId(), appleVerifyReceipt);
purchaseService.verifyAppleTicketTransaction(user.getId(), appleTransaction);

return BaseResponse.success(VERIFY_RECEIPT_SUCCESS, data);
return BaseResponse.success(VERIFY_RECEIPT_SUCCESS);
}

@Operation(summary = "구독 연장 유도 필요 여부 확인 API", responses = {
Expand All @@ -54,4 +76,5 @@ public BaseResponse<UserSubscribeNeededResponse> getUserSubscribeNeeded(
val data = purchaseService.getUserSubscribe(user, LocalDateTime.now());
return BaseResponse.success(USER_SUBSCRIBE_NEEDED_READ_SUCCESS, data);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.yello.server.domain.purchase.dto.apple;

import lombok.Builder;

@Builder
public record AppleOrderResponse(
int appAppleId,
String environment,
String JWSTransaction
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.yello.server.domain.purchase.dto.apple;

import lombok.Builder;

@Builder
public record AppleTransaction(

String transactionId,
String productId

) {

}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
public enum ProductType {
YELLO_PLUS("yello_plus"),
ONE_TICKET("one_ticket"),
TWO_TICKET("two_ticket"),
FIVE_TICKET("five_ticket");

private final String intial;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.yello.server.domain.user.entity.User;
import com.yello.server.global.common.dto.AuditingTimeEntity;
import com.yello.server.global.common.util.ConstantUtil;
import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.Entity;
Expand Down Expand Up @@ -44,4 +45,32 @@ public class Purchase extends AuditingTimeEntity {
@Convert(converter = ProductTypeConverter.class)
private ProductType productType;

public static Purchase of(User user, ProductType productType, Gateway gateway) {
return Purchase.builder()
.price(setPrice(productType.toString()))
.user(user)
.gateway(gateway)
.productType(productType)
.build();
}

public static int setPrice(String productType) {
switch (productType) {
case "YELLO_PLUS":
return ConstantUtil.YELLO_PLUS;
case "ONE_TICKET":
return ConstantUtil.ONE_TICKET;
case "TWO_TICKET":
return ConstantUtil.TWO_TICKET;
case "FIVE_TICKET":
return ConstantUtil.FIVE_TICKET;
default:
return 0;
}
}

public static Purchase createPurchase(User user, ProductType productType, Gateway gateway) {
return Purchase.of(user, productType, gateway);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.yello.server.domain.purchase.exception;

import com.yello.server.global.common.ErrorCode;
import com.yello.server.global.exception.CustomException;
import lombok.Getter;

@Getter
public class PurchaseException extends CustomException {

public PurchaseException(ErrorCode error) {
super(error, "[PurchaseException] " + error.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.yello.server.domain.purchase.exception;

import com.yello.server.global.common.ErrorCode;
import com.yello.server.global.exception.CustomException;
import lombok.Getter;

@Getter
public class PurchaseNotFoundException extends CustomException {

public PurchaseNotFoundException(ErrorCode error) {
super(error, "[PurchaseNotFoundException] " + error.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.yello.server.domain.purchase.exception;

import com.yello.server.global.common.ErrorCode;
import com.yello.server.global.exception.CustomException;
import lombok.Getter;

@Getter
public class SubscriptionConflictException extends CustomException {

public SubscriptionConflictException(ErrorCode error) {
super(error, "[SubscriptionConflictException] " + error.getMessage());
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
package com.yello.server.domain.purchase.service;

import com.yello.server.domain.purchase.dto.apple.AppleVerifyReceipt;
import com.yello.server.domain.purchase.dto.apple.AppleVerifyReceiptResponse;
import static com.yello.server.global.common.ErrorCode.NOT_FOUND_TRANSACTION_EXCEPTION;
import static com.yello.server.global.common.ErrorCode.SUBSCRIBE_ACTIVE_EXCEPTION;
import static com.yello.server.global.common.util.ConstantUtil.FIVE_TICKET_ID;
import static com.yello.server.global.common.util.ConstantUtil.ONE_TICKET_ID;
import static com.yello.server.global.common.util.ConstantUtil.TWO_TICKET_ID;
import static com.yello.server.global.common.util.ConstantUtil.YELLO_PLUS_ID;

import com.yello.server.domain.purchase.dto.apple.AppleOrderResponse;
import com.yello.server.domain.purchase.dto.apple.AppleTransaction;
import com.yello.server.domain.purchase.dto.response.UserSubscribeNeededResponse;
import com.yello.server.domain.purchase.entity.Gateway;
import com.yello.server.domain.purchase.entity.ProductType;
import com.yello.server.domain.purchase.entity.Purchase;
import com.yello.server.domain.purchase.exception.PurchaseException;
import com.yello.server.domain.purchase.exception.SubscriptionConflictException;
import com.yello.server.domain.purchase.repository.PurchaseRepository;
import com.yello.server.domain.user.entity.Subscribe;
import com.yello.server.domain.user.entity.User;
Expand All @@ -28,11 +38,6 @@ public class PurchaseService {
private final PurchaseRepository purchaseRepository;
private final AppleUtil appleUtil;

public AppleVerifyReceiptResponse verifyReceipt(Long userId, AppleVerifyReceipt request) {
return appleUtil.appleVerifyReceipt(request);

}

public UserSubscribeNeededResponse getUserSubscribe(User user, LocalDateTime time) {
final Optional<Purchase> mostRecentPurchase =
purchaseRepository.findTopByUserAndProductTypeOrderByCreatedAtDesc(
Expand All @@ -44,4 +49,63 @@ public UserSubscribeNeededResponse getUserSubscribe(User user, LocalDateTime tim

return UserSubscribeNeededResponse.of(user, isSubscribeNeeded);
}

@Transactional
public void verifyAppleSubscriptionTransaction(Long userId,
AppleTransaction request) {
final AppleOrderResponse verifyReceiptResponse = appleUtil.appleGetTransaction(request);
final User user = userRepository.getById(userId);

if (user.getSubscribe() == Subscribe.ACTIVE) {
throw new SubscriptionConflictException(SUBSCRIBE_ACTIVE_EXCEPTION);
}

if (request.productId() == YELLO_PLUS_ID) {
createSubscribe(user);
user.ticketPlus(3);
}

throw new PurchaseException(NOT_FOUND_TRANSACTION_EXCEPTION);
}

@Transactional
public void verifyAppleTicketTransaction(Long userId, AppleTransaction request) {
final AppleOrderResponse verifyReceiptResponse = appleUtil.appleGetTransaction(request);
final User user = userRepository.getById(userId);

// 정상적인 구매일 경우
switch (request.productId()) {
case ONE_TICKET_ID:
createTicket(user, ProductType.ONE_TICKET);
user.ticketPlus(1);
break;
case TWO_TICKET_ID:
createTicket(user, ProductType.TWO_TICKET);
user.ticketPlus(2);
break;
case FIVE_TICKET_ID:
createTicket(user, ProductType.FIVE_TICKET);
user.ticketPlus(5);
break;
default:
throw new PurchaseException(NOT_FOUND_TRANSACTION_EXCEPTION);

}
}

@Transactional
public void createSubscribe(User user) {

user.setSubscribe();
Purchase newPurchase = Purchase.createPurchase(user, ProductType.YELLO_PLUS, Gateway.APPLE);

purchaseRepository.save(newPurchase);
}

@Transactional
public void createTicket(User user, ProductType productType) {

Purchase newPurchase = Purchase.createPurchase(user, productType, Gateway.APPLE);
purchaseRepository.save(newPurchase);
}
}
7 changes: 7 additions & 0 deletions src/main/java/com/yello/server/domain/user/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -182,5 +182,12 @@ public void setDeviceToken(String deviceToken) {
this.deviceToken = deviceToken;
}

public void setSubscribe() {
this.subscribe = Subscribe.ACTIVE;
}

public void ticketPlus(int ticketCount) {
this.ticketCount += ticketCount;
}

}
3 changes: 3 additions & 0 deletions src/main/java/com/yello/server/global/common/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ public enum ErrorCode {
NOT_FOUND_QUESTION_EXCEPTION(NOT_FOUND, "존재하지 않는 질문입니다."),
NOT_FOUND_FRIEND_EXCEPTION(NOT_FOUND, "존재하지 않는 친구이거나 친구 관계가 아닙니다."),
REDIS_NOT_FOUND_UUID(NOT_FOUND, "uuid에 해당하는 디바이스 토큰 정보를 찾을 수 없습니다."),
NOT_FOUND_TRANSACTION_EXCEPTION(NOT_FOUND, "존재하지 않는 거래입니다"),
NOT_FOUND_PRODUCT_ID_EXCEPTION(NOT_FOUND, "존재하지 않는 상품 아이디 입니다"),

/**
* 409 CONFLICT
Expand All @@ -73,6 +75,7 @@ public enum ErrorCode {
YELLOID_CONFLICT_USER_EXCEPTION(CONFLICT, "이미 존재하는 yelloId 입니다."),
UUID_CONFLICT_USER_EXCEPTION(CONFLICT, "이미 존재하는 소셜 유저입니다."),
DEVICE_TOKEN_CONFLICT_USER_EXCEPTION(CONFLICT, "이미 존재하는 deviceToken 입니다."),
SUBSCRIBE_ACTIVE_EXCEPTION(CONFLICT, "이미 옐로플러스 구독한 유저입니다."),

/**
* 500 INTERNAL SERVER ERROR
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.yello.server.global.common.factory;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.security.KeyFactory;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Calendar;
import java.util.Date;
import lombok.SneakyThrows;
import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class TokenFactory {

@Value("${kid}")
private String KID;
@Value("${iss}")
private String ISS;
@Value("${aud}")
private String AUD;
@Value("${bid}")
private String BID;
@Value("${sig}")
private String SIG;

@SneakyThrows
public String generateAppleToken() {
String jws = Jwts.builder()
// header
.setHeaderParam("kid", KID)
// payload
.setIssuer(ISS)
.setIssuedAt(new Date(Calendar.getInstance().getTimeInMillis())) // 발행 시간 - UNIX 시간
.setExpiration(
new Date(Calendar.getInstance().getTimeInMillis() + (3 * 60
* 1000))) // 만료 시간 (발행 시간 + 3분)
.setAudience(AUD)
.claim("bid", BID)
// sign
.signWith(SignatureAlgorithm.ES256,
KeyFactory.getInstance("EC").generatePrivate(new PKCS8EncodedKeySpec(
Base64.decodeBase64(SIG))))
.compact();

return jws;
}
}
Loading

0 comments on commit 20473b8

Please sign in to comment.