-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Added payment mvc (토스) #114
Changes from 1 commit
895681c
83a8c8c
ed377f7
a3eed29
aab034a
c7ca5fb
5b74cff
8cb03b7
da147d2
77dd357
3dac6ee
027d662
16d1c69
6ca6b30
dd49854
1683947
6499d91
125802f
0623f32
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package com.f_lab.joyeuse_planete.payment.controller; | ||
|
||
import com.f_lab.joyeuse_planete.core.exceptions.ErrorCode; | ||
import com.f_lab.joyeuse_planete.core.exceptions.JoyeusePlaneteApplicationException; | ||
import com.f_lab.joyeuse_planete.core.util.log.LogUtil; | ||
import com.f_lab.joyeuse_planete.payment.service.PaymentService; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RequestParam; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
import java.math.BigDecimal; | ||
|
||
@RestController | ||
@RequiredArgsConstructor | ||
@RequestMapping("/api/v1/payment") | ||
public class PaymentController { | ||
|
||
private static final String TOSS = "토스"; | ||
|
||
private final PaymentService paymentService; | ||
|
||
@GetMapping("/toss-success") | ||
public void tossSuccess(@RequestParam("paymentKey") String paymentKey, | ||
@RequestParam("orderId") Long orderId, | ||
@RequestParam("amount") BigDecimal amount) { | ||
|
||
paymentService.processPaymentSuccess(paymentKey, orderId, amount, TOSS); | ||
} | ||
|
||
@GetMapping("/toss-fail") | ||
public void tossFail(@RequestParam(value = "orderId", required = false) Long orderId, | ||
@RequestParam("code") String code, | ||
@RequestParam("message") String message) { | ||
|
||
if (orderId == null) { | ||
LogUtil.exception("PaymentController.tossFail", new JoyeusePlaneteApplicationException(ErrorCode.ORDER_NOT_PROCESSED_EXCEPTION_CUSTOMER)); | ||
return; | ||
} | ||
|
||
paymentService.processPaymentFailure(orderId, code, message, TOSS); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,16 @@ | ||
package com.f_lab.joyeuse_planete.payment.repository; | ||
|
||
import com.f_lab.joyeuse_planete.core.domain.Payment; | ||
|
||
import org.springframework.data.jpa.repository.JpaRepository; | ||
import org.springframework.data.jpa.repository.Query; | ||
import org.springframework.data.repository.query.Param; | ||
|
||
import java.math.BigDecimal; | ||
|
||
|
||
public interface PaymentRepository extends JpaRepository<Payment, Long> { | ||
|
||
@Query(value = "INSERT INTO Payment p (payment_key, processor, order_id, total_cost, status) VALUES (:paymentKey, :processor, :orderId, totalCost, status)", nativeQuery = true) | ||
Payment save(@Param("paymentKey") String paymentKey, @Param("processor") String processor, @Param("orderId") Long orderId, @Param("totalCost") BigDecimal totalCost, @Param("status") String status); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,69 @@ | ||
package com.f_lab.joyeuse_planete.payment.service; | ||
|
||
import com.f_lab.joyeuse_planete.core.domain.Payment; | ||
import com.f_lab.joyeuse_planete.core.domain.PaymentStatus; | ||
import com.f_lab.joyeuse_planete.core.events.PaymentProcessedEvent; | ||
import com.f_lab.joyeuse_planete.core.exceptions.ErrorCode; | ||
import com.f_lab.joyeuse_planete.core.exceptions.JoyeusePlaneteApplicationException; | ||
import com.f_lab.joyeuse_planete.core.kafka.service.KafkaService; | ||
import com.f_lab.joyeuse_planete.core.util.log.LogUtil; | ||
import com.f_lab.joyeuse_planete.payment.repository.PaymentRepository; | ||
import io.micrometer.core.annotation.Timed; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
import java.math.BigDecimal; | ||
|
||
@Timed("payment") | ||
@Service | ||
@Transactional(readOnly = true) | ||
@RequiredArgsConstructor | ||
public class PaymentService { | ||
|
||
public void process() { | ||
private final PaymentRepository paymentRepository; | ||
private final KafkaService kafkaService; | ||
|
||
@Value("${payment.events.topics.process}") | ||
private String PAYMENT_PROCESS_EVENT; | ||
|
||
@Transactional | ||
public void processPaymentSuccess(String paymentKey, Long orderId, BigDecimal amount, String processor) { | ||
try { | ||
paymentRepository.save(paymentKey, processor, orderId, amount, PaymentStatus.DONE.toString()); | ||
|
||
// "https://api.tosspayments.com/v1/payments/confirm" 에 결제성공 로직 구현 | ||
} catch (Exception e) { | ||
LogUtil.exception("PaymentService.processPaymentSuccess", e); | ||
throw new RuntimeException(e); | ||
} | ||
|
||
sendKafkaPaymentProcessedEvent(null); | ||
} | ||
|
||
@Transactional | ||
public void processPaymentFailure(Long orderId, String code, String message, String processor) { | ||
try { | ||
paymentRepository.save("null", processor, orderId, BigDecimal.ZERO, PaymentStatus.ABORTED.toString()); | ||
|
||
} catch(Exception e) { | ||
LogUtil.exception("PaymentService.processPaymentFailure", e); | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
|
||
public void sendKafkaPaymentProcessedEvent(PaymentProcessedEvent event) { | ||
try { | ||
kafkaService.sendKafkaEvent(PAYMENT_PROCESS_EVENT, event); | ||
} catch(Exception e) { | ||
LogUtil.exception("PaymentService.sendKafkaEvent", e); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 로깅 이외 다른 처리는 없어도 되는걸까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이것도 오래 생각을 해보았는데 여기서 예외를 던져버리면 모든 transaction이 rollback 이 되어버리기 때문에 로깅 처리만 하고 재시도 로직은 카프카 dead letter topic queue 에서 처리되게 하는 것이 맞다고 생각하여 이렇게 처리하였습니다. |
||
} | ||
} | ||
|
||
private Payment findPaymentById(Long paymentId) { | ||
return paymentRepository.findById(paymentId).orElseThrow( | ||
() -> new JoyeusePlaneteApplicationException(ErrorCode.PAYMENT_NOT_EXIST_EXCEPTION) | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,33 +1,66 @@ | ||
package com.f_lab.joyeuse_planete.payment.service.handler; | ||
|
||
import com.f_lab.joyeuse_planete.core.events.OrderCreatedEvent; | ||
|
||
import com.f_lab.joyeuse_planete.core.events.PaymentProcessedEvent; | ||
import com.f_lab.joyeuse_planete.core.events.PaymentProcessingFailedEvent; | ||
import com.f_lab.joyeuse_planete.core.kafka.exceptions.RetryableException; | ||
import com.f_lab.joyeuse_planete.core.kafka.service.KafkaService; | ||
|
||
import com.f_lab.joyeuse_planete.core.kafka.util.ExceptionUtil; | ||
import com.f_lab.joyeuse_planete.core.util.log.LogUtil; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.kafka.annotation.KafkaHandler; | ||
import org.springframework.kafka.annotation.KafkaListener; | ||
import org.springframework.kafka.support.KafkaHeaders; | ||
import org.springframework.messaging.handler.annotation.Header; | ||
import org.springframework.messaging.handler.annotation.Payload; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.util.Objects; | ||
|
||
import static com.f_lab.joyeuse_planete.core.util.time.TimeConstantsString.FIVE_SECONDS; | ||
|
||
|
||
@Slf4j | ||
@Component | ||
@RequiredArgsConstructor | ||
@KafkaListener(topics = "${payments.dead-letter-topic.name}", groupId = "${spring.kafka.consumer.group-id}") | ||
@KafkaListener(topics = "${payment.dead-letter-topic}", groupId = "${spring.kafka.consumer.group-id}") | ||
public class PaymentDeadLetterTopicHandler { | ||
|
||
private final KafkaService kafkaService; | ||
|
||
@KafkaHandler | ||
public void processDeadOrderCreatedEvent(@Payload OrderCreatedEvent orderCreatedEvent, | ||
@Header(value = KafkaHeaders.EXCEPTION_FQCN, required = false) String exceptionName, | ||
@Header(value = KafkaHeaders.EXCEPTION_MESSAGE, required = false) String exceptionMessage, | ||
@Header(value = KafkaHeaders.ORIGINAL_TOPIC, required = false) String originalTopic) { | ||
public void processDeadPaymentProcessedEvent(@Payload PaymentProcessedEvent paymentProcessedEvent, | ||
@Header(value = KafkaHeaders.EXCEPTION_FQCN, required = false) String exceptionName, | ||
@Header(value = KafkaHeaders.EXCEPTION_MESSAGE, required = false) String exceptionMessage, | ||
@Header(value = KafkaHeaders.ORIGINAL_TOPIC, required = false) String originalTopic) { | ||
|
||
handleDeadEventsForRetries(paymentProcessedEvent, exceptionName, exceptionMessage, originalTopic); | ||
} | ||
|
||
@KafkaHandler | ||
public void processDeadPaymentProcessingFailedEvent(@Payload PaymentProcessingFailedEvent paymentProcessingFailedEvent, | ||
@Header(value = KafkaHeaders.EXCEPTION_FQCN, required = false) String exceptionName, | ||
@Header(value = KafkaHeaders.EXCEPTION_MESSAGE, required = false) String exceptionMessage, | ||
@Header(value = KafkaHeaders.ORIGINAL_TOPIC, required = false) String originalTopic) { | ||
|
||
handleDeadEventsForRetries(paymentProcessingFailedEvent, exceptionName, exceptionMessage, originalTopic); | ||
} | ||
|
||
private void handleDeadEventsForRetries(Object event, String exceptionName, String exceptionMessage, String originalTopic) { | ||
if (Objects.isNull(exceptionMessage) || | ||
Objects.isNull(originalTopic) || | ||
ExceptionUtil.noRequeue(exceptionMessage) | ||
) { | ||
LogUtil.deadLetterMissingFormats(exceptionName, exceptionMessage, originalTopic); | ||
return; | ||
} | ||
|
||
try { | ||
Thread.sleep(Integer.parseInt(FIVE_SECONDS)); | ||
} catch (InterruptedException e) { | ||
throw new RetryableException(); | ||
} | ||
|
||
// TODO: THINK ABOUT THE LOGICS; | ||
kafkaService.sendKafkaEvent(originalTopic, event); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
server: | ||
port: 8080 | ||
tomcat: | ||
mbeanregistry: | ||
enabled: true | ||
|
||
management: | ||
endpoints: | ||
web: | ||
exposure: | ||
include: "*" | ||
exclude: "env, beans" | ||
info: | ||
java: | ||
enabled: true | ||
os: | ||
enabled: true | ||
|
||
prometheus: | ||
metrics: | ||
export: | ||
pushgateway: | ||
enabled: true | ||
base-url: ${MONITORING_SERVER_IP} | ||
push-rate: 30s | ||
job: orders-service | ||
enabled: true | ||
|
||
spring: | ||
kafka: | ||
bootstrap-servers: ${KAFKA_SERVER_IP} | ||
|
||
datasource: | ||
url: ${DATABASE_URL} | ||
username: ${DATABASE_USERNAME} | ||
password: ${DATABASE_PASSWORD} | ||
driver-class-name: com.mysql.cj.jdbc.Driver | ||
|
||
jpa: | ||
database-platform: org.hibernate.dialect.MySQLDialect | ||
hibernate: | ||
ddl-auto: validate | ||
|
||
logging: | ||
level: | ||
org.hibernate.sql: ERROR |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
추후 더 명확한 예외로 처리하시면 됩니다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵 현재는 비동기를 적용하여 exception을 적용하도록 해두었습니다.