├── settings.gradle ├── src ├── main │ ├── java │ │ └── flab │ │ │ └── payment_system │ │ │ ├── domain │ │ │ ├── order │ │ │ │ ├── dto │ │ │ │ │ ├── OrderDto.java │ │ │ │ │ ├── OrderDetailDto.java │ │ │ │ │ ├── OrderProductDto.java │ │ │ │ │ └── OrderCancelDto.java │ │ │ │ ├── repository │ │ │ │ │ ├── OrderCustomRepository.java │ │ │ │ │ ├── OrderCustomRepositoryImpl.java │ │ │ │ │ └── OrderRepository.java │ │ │ │ ├── exception │ │ │ │ │ └── OrderNotExistBadRequestException.java │ │ │ │ ├── entity │ │ │ │ │ └── OrderProduct.java │ │ │ │ ├── service │ │ │ │ │ └── OrderService.java │ │ │ │ └── controller │ │ │ │ │ └── OrderController.java │ │ │ ├── payment │ │ │ │ ├── response │ │ │ │ │ ├── PaymentCancelDto.java │ │ │ │ │ ├── toss │ │ │ │ │ │ ├── PaymentToss.java │ │ │ │ │ │ ├── Settlement.java │ │ │ │ │ │ └── PaymentTossDtoImpl.java │ │ │ │ │ ├── PaymentApprovalDto.java │ │ │ │ │ ├── PaymentOrderDetailDto.java │ │ │ │ │ ├── kakao │ │ │ │ │ │ ├── PaymentKakao.java │ │ │ │ │ │ ├── PaymentKakaoReadyDtoImpl.java │ │ │ │ │ │ ├── PaymentKakaoApprovalDtoImpl.java │ │ │ │ │ │ ├── PaymentKakaoOrderDetailDtoImpl.java │ │ │ │ │ │ └── PaymentKakaoCancelDtoImpl.java │ │ │ │ │ └── PaymentReadyDto.java │ │ │ │ ├── repository │ │ │ │ │ ├── toss │ │ │ │ │ │ ├── TossPaymentCustomRepository.java │ │ │ │ │ │ ├── TossPaymentRepository.java │ │ │ │ │ │ └── TossPaymentCustomRepositoryImpl.java │ │ │ │ │ ├── kakao │ │ │ │ │ │ ├── KakaoPaymentCustomRepository.java │ │ │ │ │ │ ├── KakaoPaymentRepository.java │ │ │ │ │ │ └── KaKaoPaymentCustomRepositoryImpl.java │ │ │ │ │ ├── PaymentCustomRepository.java │ │ │ │ │ ├── PaymentRepository.java │ │ │ │ │ └── PaymentCustomRepositoryImpl.java │ │ │ │ ├── entity │ │ │ │ │ ├── toss │ │ │ │ │ │ ├── Checkout.java │ │ │ │ │ │ ├── Receipt.java │ │ │ │ │ │ ├── Discount.java │ │ │ │ │ │ ├── Fee.java │ │ │ │ │ │ ├── Failure.java │ │ │ │ │ │ ├── Transfer.java │ │ │ │ │ │ ├── GiftCertificate.java │ │ │ │ │ │ ├── EasyPay.java │ │ │ │ │ │ ├── MobilePhone.java │ │ │ │ │ │ ├── RefundReceiveAccount.java │ │ │ │ │ │ ├── CashReceipt.java │ │ │ │ │ │ ├── Cancels.java │ │ │ │ │ │ ├── VirtualAccount.java │ │ │ │ │ │ ├── Card.java │ │ │ │ │ │ ├── CashReceipts.java │ │ │ │ │ │ └── TossPayment.java │ │ │ │ │ ├── kakao │ │ │ │ │ │ ├── CancelAvailableAmount.java │ │ │ │ │ │ ├── CanceledAmount.java │ │ │ │ │ │ ├── ApprovedCancelAmount.java │ │ │ │ │ │ ├── PaymentActionDetails.java │ │ │ │ │ │ ├── Amount.java │ │ │ │ │ │ ├── KakaoPayment.java │ │ │ │ │ │ └── CardInfo.java │ │ │ │ │ └── Payment.java │ │ │ │ ├── enums │ │ │ │ │ ├── PaymentPgCompany.java │ │ │ │ │ ├── PaymentStateConstant.java │ │ │ │ │ ├── PaymentKakaoEndpoint.java │ │ │ │ │ ├── PaymentPgCompanyStringToEnumConverter.java │ │ │ │ │ └── PaymentTossEndpoint.java │ │ │ │ ├── exception │ │ │ │ │ ├── PaymentFailBadRequestException.java │ │ │ │ │ ├── PaymentNotApprovedConflictException.java │ │ │ │ │ ├── PaymentNotExistBadRequestException.java │ │ │ │ │ ├── PaymentAlreadyApprovedConflictException.java │ │ │ │ │ ├── PaymentKaKaoServiceUnavailableException.java │ │ │ │ │ └── PaymentTossServiceUnavailableException.java │ │ │ │ ├── dto │ │ │ │ │ └── PaymentCreateDto.java │ │ │ │ ├── service │ │ │ │ │ ├── PaymentStrategy.java │ │ │ │ │ ├── kakao │ │ │ │ │ │ └── PaymentStrategyKaKaoService.java │ │ │ │ │ ├── toss │ │ │ │ │ │ └── PaymentStrategyTossService.java │ │ │ │ │ └── PaymentService.java │ │ │ │ ├── client │ │ │ │ │ ├── toss │ │ │ │ │ │ └── PaymentTossClient.java │ │ │ │ │ └── kakao │ │ │ │ │ │ └── PaymentKakaoClient.java │ │ │ │ ├── controller │ │ │ │ │ └── PaymentController.java │ │ │ │ └── request │ │ │ │ │ ├── toss │ │ │ │ │ └── PaymentTossRequestBodyFactory.java │ │ │ │ │ └── kakao │ │ │ │ │ └── PaymentKakaoRequestBodyFactory.java │ │ │ ├── user │ │ │ │ ├── repository │ │ │ │ │ ├── UserCustomRepository.java │ │ │ │ │ ├── UserVerificationRepository.java │ │ │ │ │ ├── UserRepository.java │ │ │ │ │ └── UserCustomRepositoryImpl.java │ │ │ │ ├── dto │ │ │ │ │ ├── UserDto.java │ │ │ │ │ ├── UserVerificationDto.java │ │ │ │ │ ├── UserVerifyEmailDto.java │ │ │ │ │ ├── UserConfirmVerificationNumberDto.java │ │ │ │ │ └── UserSignUpDto.java │ │ │ │ ├── exception │ │ │ │ │ ├── UserUnauthorizedException.java │ │ │ │ │ ├── UserVerifyUserEmailException.java │ │ │ │ │ ├── UserNotExistBadRequestException.java │ │ │ │ │ ├── UserNotSignInedConflictException.java │ │ │ │ │ ├── UserAlreadySignInConflictException.java │ │ │ │ │ ├── UserEmailNotExistBadRequestException.java │ │ │ │ │ ├── UserPasswordFailBadRequestException.java │ │ │ │ │ ├── UserVerificationUnauthorizedException.java │ │ │ │ │ ├── UserEmailAlreadyExistConflictException.java │ │ │ │ │ ├── UserSignUpBadRequestException.java │ │ │ │ │ ├── UserVerificationIdBadRequestException.java │ │ │ │ │ ├── UserVerificationEmailBadRequestException.java │ │ │ │ │ └── UserVerificationNumberBadRequestException.java │ │ │ │ ├── entity │ │ │ │ │ ├── UserVerification.java │ │ │ │ │ └── User.java │ │ │ │ ├── controller │ │ │ │ │ └── UserController.java │ │ │ │ └── service │ │ │ │ │ └── UserService.java │ │ │ ├── product │ │ │ │ ├── repository │ │ │ │ │ ├── ProductCustomRepository.java │ │ │ │ │ ├── ProductRepository.java │ │ │ │ │ └── ProductCustomRepositoryImpl.java │ │ │ │ ├── exception │ │ │ │ │ ├── ProductSoldOutException.java │ │ │ │ │ └── ProductNotExistBadRequestException.java │ │ │ │ ├── dto │ │ │ │ │ └── ProductDto.java │ │ │ │ ├── controller │ │ │ │ │ └── ProductController.java │ │ │ │ ├── entity │ │ │ │ │ └── Product.java │ │ │ │ └── service │ │ │ │ │ └── ProductService.java │ │ │ ├── session │ │ │ │ └── service │ │ │ │ │ └── SessionService.java │ │ │ ├── compensation │ │ │ │ └── batch │ │ │ │ │ ├── CompensationItemWriter.java │ │ │ │ │ ├── CompensationConfig.java │ │ │ │ │ ├── CompensationItemProcessor.java │ │ │ │ │ ├── CompensationItemReader.java │ │ │ │ │ └── CompensationJobConfig.java │ │ │ ├── log │ │ │ │ └── entity │ │ │ │ │ └── AppLogs.java │ │ │ ├── redisson │ │ │ │ └── service │ │ │ │ │ └── RedissonLockService.java │ │ │ └── mail │ │ │ │ └── service │ │ │ │ └── MailService.java │ │ │ ├── common │ │ │ ├── response │ │ │ │ └── ResponseMessage.java │ │ │ ├── exception │ │ │ │ ├── BaseException.java │ │ │ │ ├── OkException.java │ │ │ │ ├── ConflictException.java │ │ │ │ ├── NotfoundException.java │ │ │ │ ├── ForbiddenException.java │ │ │ │ ├── BadRequestException.java │ │ │ │ ├── UnauthorizedException.java │ │ │ │ ├── ServiceUnavailableException.java │ │ │ │ ├── ExceptionMessage.java │ │ │ │ └── CustomExceptionHandler.java │ │ │ ├── enums │ │ │ │ └── Constant.java │ │ │ ├── util │ │ │ │ └── DateUtil.java │ │ │ ├── data │ │ │ │ └── BaseEntity.java │ │ │ ├── interceptor │ │ │ │ └── LoggingInterceptor.java │ │ │ └── filter │ │ │ │ ├── SignInCheckFilter.java │ │ │ │ └── ExceptionHandlerFilter.java │ │ │ ├── adapter │ │ │ ├── RedissonLockAdapter.java │ │ │ ├── PaymentAdapter.java │ │ │ ├── UserAdapter.java │ │ │ ├── OrderAdapter.java │ │ │ ├── RedissonLockAdapterImpl.java │ │ │ ├── UserAdapterImpl.java │ │ │ ├── PaymentAdapterImpl.java │ │ │ └── OrderAdapterImpl.java │ │ │ ├── config │ │ │ ├── QueryDslConfig.java │ │ │ ├── WebConfig.java │ │ │ ├── PaymentStrategyConfig.java │ │ │ ├── RedisConfig.java │ │ │ └── AppConfig.java │ │ │ └── PaymentSystemApplication.java │ └── resources │ │ ├── payment_kakao.csv │ │ ├── payment_toss.csv │ │ ├── templates │ │ └── mail.html │ │ ├── application-test.yml │ │ ├── application.yml │ │ ├── log4j2-prod.xml │ │ └── log4j2-local.xml └── test │ └── java │ └── flab │ └── payment_system │ ├── PaymentSystemApplicationTests.java │ ├── config │ └── DatabaseCleanUp.java │ ├── order │ └── service │ │ └── OrderProductServiceIntegrationTest.java │ ├── user │ └── service │ │ └── UserServiceIntegrationTest.java │ ├── product │ └── service │ │ └── ProductServiceIntegrationTest.java │ ├── payment │ └── service │ │ └── PaymentServiceIntegrationTest.java │ └── batch │ └── compensation │ └── BatchIntegrationTest.java ├── config └── naver-checkstyle-suppressions.xml ├── .editorconfig ├── .gitignore └── .github └── workflows └── sonarcloud-analyze.yml /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'payment-system' 2 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/order/dto/OrderDto.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.order.dto; 2 | 3 | public record OrderDto(Long orderId) { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/common/response/ResponseMessage.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.common.response; 2 | 3 | public record ResponseMessage(String message) { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/response/PaymentCancelDto.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.response; 2 | 3 | public interface PaymentCancelDto { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/response/toss/PaymentToss.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.response.toss; 2 | 3 | public interface PaymentToss { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/response/PaymentApprovalDto.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.response; 2 | 3 | public interface PaymentApprovalDto { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/repository/UserCustomRepository.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.repository; 2 | 3 | public interface UserCustomRepository { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/order/repository/OrderCustomRepository.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.order.repository; 2 | 3 | public interface OrderCustomRepository { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/response/PaymentOrderDetailDto.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.response; 2 | 3 | public interface PaymentOrderDetailDto { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/response/kakao/PaymentKakao.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.response.kakao; 2 | 3 | 4 | public interface PaymentKakao { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/dto/UserDto.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.dto; 2 | 3 | public record UserDto( 4 | String email, 5 | String password 6 | ) { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/order/dto/OrderDetailDto.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.order.dto; 2 | 3 | public record OrderDetailDto(String tid) { 4 | 5 | } 6 | 7 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/repository/toss/TossPaymentCustomRepository.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.repository.toss; 2 | 3 | public interface TossPaymentCustomRepository { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/repository/kakao/KakaoPaymentCustomRepository.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.repository.kakao; 2 | 3 | 4 | public interface KakaoPaymentCustomRepository { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /config/naver-checkstyle-suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/response/PaymentReadyDto.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.response; 2 | 3 | public interface PaymentReadyDto { 4 | void setPaymentId(Long paymentId); 5 | String getPaymentKey(); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/resources/payment_kakao.csv: -------------------------------------------------------------------------------- 1 | orderId,paymentKey,totalAmount,paymentState,taxFreeAmount,installMonth 2 | 1,Key123,50000,1,0,3 3 | 10002,Key124,75000,2,5000,0 4 | 10003,Key125,25000,1,0,6 5 | 10004,Key126,30000,3,0,12 6 | 10005,Key127,45000,1,4500,0 7 | -------------------------------------------------------------------------------- /src/main/resources/payment_toss.csv: -------------------------------------------------------------------------------- 1 | orderId,paymentKey,totalAmount,paymentState,taxFreeAmount,installMonth 2 | 10001,Key123,50000,1,0,3 3 | 10002,Key124,75000,2,5000,0 4 | 10003,Key125,25000,1,0,6 5 | 10004,Key126,30000,3,0,12 6 | 10005,Key127,45000,1,4500,0 7 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/toss/Checkout.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.toss; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class Checkout { 9 | private String url; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/toss/Receipt.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.toss; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class Receipt { 9 | 10 | private String url; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/toss/Discount.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.toss; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class Discount { 9 | 10 | private Integer amount; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/dto/UserVerificationDto.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.dto; 2 | 3 | 4 | public record UserVerificationDto( 5 | Long verificationId, 6 | Integer verificationNumber, 7 | String email, 8 | boolean isVerified) { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/toss/Fee.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.toss; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class Fee { 9 | private String type; 10 | private Integer fee; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/toss/Failure.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.toss; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class Failure { 9 | 10 | private String code; 11 | private String message; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/adapter/RedissonLockAdapter.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.adapter; 2 | 3 | public interface RedissonLockAdapter { 4 | void checkRemainStock(Long productId); 5 | 6 | void decreaseStock(Long productId, Integer quantity); 7 | 8 | void increaseStock(Long productId, Integer quantity); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/toss/Transfer.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.toss; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class Transfer { 9 | 10 | private String bankCode; 11 | private String settlementStatus; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/common/exception/BaseException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.common.exception; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class BaseException extends RuntimeException { 7 | 8 | protected String status; 9 | 10 | protected String message; 11 | protected int code; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/common/enums/Constant.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.common.enums; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @RequiredArgsConstructor 7 | @Getter 8 | public enum Constant { 9 | API_AND_VERSION("/api/v1"); 10 | 11 | 12 | private final String value; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/toss/GiftCertificate.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.toss; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class GiftCertificate { 9 | 10 | private String approveNo; 11 | private String settlementStatus; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/common/util/DateUtil.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.common.util; 2 | 3 | import java.time.OffsetDateTime; 4 | 5 | public class DateUtil { 6 | public static String getYesterdayDate() { 7 | OffsetDateTime yesterday = OffsetDateTime.now().minusDays(1); 8 | return yesterday.toLocalDate().toString(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/toss/EasyPay.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.toss; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class EasyPay { 9 | 10 | private String provider; 11 | private Integer amount; 12 | private Integer discountAmount; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/repository/PaymentCustomRepository.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.repository; 2 | 3 | public interface PaymentCustomRepository { 4 | 5 | long updatePaymentStateByPaymentId(Long paymentId, Integer state); 6 | 7 | long updatePaymentStateByOrderId(Long paymentId, Integer state); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/common/exception/OkException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.common.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | 5 | public class OkException extends BaseException{ 6 | protected OkException() { 7 | this.status = String.valueOf(HttpStatus.OK); 8 | this.code = HttpStatus.OK.value(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/product/repository/ProductCustomRepository.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.product.repository; 2 | 3 | import flab.payment_system.domain.product.entity.Product; 4 | 5 | import java.util.List; 6 | 7 | public interface ProductCustomRepository { 8 | List findByCursor(Long lastProductId, long size); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/order/dto/OrderProductDto.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.order.dto; 2 | 3 | import jakarta.validation.constraints.Min; 4 | 5 | public record OrderProductDto 6 | ( 7 | @Min(value = 1, message = "invalid_product_id") 8 | Long productId, 9 | @Min(value = 1, message = "invalid_quantity") 10 | Integer quantity) { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/toss/MobilePhone.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.toss; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class MobilePhone { 9 | 10 | private String customerMobilePhone; 11 | private String settlementStatus; 12 | private String receiptUrl; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/product/repository/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.product.repository; 2 | 3 | import flab.payment_system.domain.product.entity.Product; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface ProductRepository extends JpaRepository, ProductCustomRepository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/repository/UserVerificationRepository.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.repository; 2 | 3 | import flab.payment_system.domain.user.entity.UserVerification; 4 | import org.springframework.data.repository.CrudRepository; 5 | 6 | public interface UserVerificationRepository extends CrudRepository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/order/repository/OrderCustomRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.order.repository; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.stereotype.Repository; 5 | 6 | @RequiredArgsConstructor 7 | @Repository 8 | public class OrderCustomRepositoryImpl implements OrderCustomRepository { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/toss/RefundReceiveAccount.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.toss; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class RefundReceiveAccount { 9 | 10 | private String bankCode; 11 | private String accountNumber; 12 | private String holderName; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/product/exception/ProductSoldOutException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.product.exception; 2 | 3 | import flab.payment_system.common.exception.OkException; 4 | 5 | public class ProductSoldOutException extends OkException { 6 | 7 | public ProductSoldOutException() { 8 | super(); 9 | this.message = "product_sold_out"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/common/exception/ConflictException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.common.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | 5 | public class ConflictException extends BaseException { 6 | 7 | protected ConflictException() { 8 | this.status = String.valueOf(HttpStatus.CONFLICT); 9 | this.code = HttpStatus.CONFLICT.value(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/common/exception/NotfoundException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.common.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | 5 | public class NotfoundException extends BaseException { 6 | 7 | protected NotfoundException() { 8 | this.status = String.valueOf(HttpStatus.NOT_FOUND); 9 | this.code = HttpStatus.NOT_FOUND.value(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/enums/PaymentPgCompany.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.enums; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @RequiredArgsConstructor 7 | @Getter 8 | public enum PaymentPgCompany { 9 | KAKAO(0, "kakao"), 10 | TOSS(1, "toss"); 11 | private final int value; 12 | private final String name; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/common/exception/ForbiddenException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.common.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | 5 | public class ForbiddenException extends BaseException { 6 | 7 | protected ForbiddenException() { 8 | this.status = String.valueOf(HttpStatus.FORBIDDEN); 9 | this.code = HttpStatus.FORBIDDEN.value(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/common/exception/BadRequestException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.common.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | 5 | public class BadRequestException extends BaseException { 6 | 7 | protected BadRequestException() { 8 | this.status = String.valueOf(HttpStatus.BAD_REQUEST); 9 | this.code = HttpStatus.BAD_REQUEST.value(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/enums/PaymentStateConstant.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.enums; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @Getter 7 | @RequiredArgsConstructor 8 | public enum PaymentStateConstant { 9 | ONGOING(0), 10 | 11 | APPROVED(1), 12 | FAIL(2), 13 | CANCEL(3); 14 | private final Integer value; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/exception/UserUnauthorizedException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.exception; 2 | 3 | import flab.payment_system.common.exception.UnauthorizedException; 4 | 5 | 6 | public class UserUnauthorizedException extends UnauthorizedException { 7 | 8 | public UserUnauthorizedException() { 9 | super(); 10 | this.message = "unauthorized"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/exception/UserVerifyUserEmailException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.exception; 2 | 3 | import flab.payment_system.common.exception.BadRequestException; 4 | 5 | 6 | public class UserVerifyUserEmailException extends BadRequestException { 7 | public UserVerifyUserEmailException() { 8 | super(); 9 | this.message = "send_mail_fail"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/flab/payment_system/PaymentSystemApplicationTests.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest(properties = {"spring.config.location=classpath:application-test.yml"}) 7 | class PaymentSystemApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/common/exception/UnauthorizedException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.common.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | 5 | public class UnauthorizedException extends BaseException { 6 | 7 | protected UnauthorizedException() { 8 | this.status = String.valueOf(HttpStatus.UNAUTHORIZED); 9 | this.code = HttpStatus.UNAUTHORIZED.value(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/exception/UserNotExistBadRequestException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.exception; 2 | 3 | import flab.payment_system.common.exception.BadRequestException; 4 | 5 | public class UserNotExistBadRequestException extends BadRequestException { 6 | public UserNotExistBadRequestException() { 7 | super(); 8 | this.message = "user_not_exist"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/exception/PaymentFailBadRequestException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.exception; 2 | 3 | import flab.payment_system.common.exception.BadRequestException; 4 | 5 | public class PaymentFailBadRequestException extends BadRequestException { 6 | public PaymentFailBadRequestException() { 7 | super(); 8 | this.message = "payment_fail"; 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/repository/kakao/KakaoPaymentRepository.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.repository.kakao; 2 | 3 | import flab.payment_system.domain.payment.entity.kakao.KakaoPayment; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface KakaoPaymentRepository extends JpaRepository, 7 | KakaoPaymentCustomRepository { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/exception/UserNotSignInedConflictException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.exception; 2 | 3 | import flab.payment_system.common.exception.ConflictException; 4 | 5 | public class UserNotSignInedConflictException extends ConflictException { 6 | 7 | public UserNotSignInedConflictException() { 8 | super(); 9 | this.message = "user_not_sign_in"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/order/exception/OrderNotExistBadRequestException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.order.exception; 2 | 3 | import flab.payment_system.common.exception.BadRequestException; 4 | 5 | public class OrderNotExistBadRequestException extends BadRequestException { 6 | 7 | public OrderNotExistBadRequestException() { 8 | super(); 9 | this.message = "order_not_exist"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/repository/toss/TossPaymentRepository.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.repository.toss; 2 | 3 | import flab.payment_system.domain.payment.entity.toss.TossPayment; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface TossPaymentRepository extends JpaRepository, 7 | TossPaymentCustomRepository { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/exception/UserAlreadySignInConflictException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.exception; 2 | 3 | import flab.payment_system.common.exception.ConflictException; 4 | 5 | 6 | public class UserAlreadySignInConflictException extends ConflictException { 7 | public UserAlreadySignInConflictException() { 8 | super(); 9 | this.message = "user_already_sign_in"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/exception/PaymentNotApprovedConflictException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.exception; 2 | 3 | import flab.payment_system.common.exception.ConflictException; 4 | 5 | public class PaymentNotApprovedConflictException extends ConflictException { 6 | public PaymentNotApprovedConflictException() { 7 | super(); 8 | this.message = "payment_not_approved"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/exception/PaymentNotExistBadRequestException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.exception; 2 | 3 | import flab.payment_system.common.exception.BadRequestException; 4 | 5 | public class PaymentNotExistBadRequestException extends BadRequestException { 6 | public PaymentNotExistBadRequestException() { 7 | super(); 8 | this.message = "payment_not_exist"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.repository; 2 | 3 | import flab.payment_system.domain.user.entity.User; 4 | import java.util.Optional; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface UserRepository extends JpaRepository, UserCustomRepository { 8 | Optional findByEmail(String email); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/product/exception/ProductNotExistBadRequestException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.product.exception; 2 | 3 | import flab.payment_system.common.exception.BadRequestException; 4 | 5 | public class ProductNotExistBadRequestException extends BadRequestException { 6 | 7 | public ProductNotExistBadRequestException() { 8 | super(); 9 | this.message = "product_not_exist"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/exception/UserEmailNotExistBadRequestException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.exception; 2 | 3 | import flab.payment_system.common.exception.BadRequestException; 4 | 5 | public class UserEmailNotExistBadRequestException extends BadRequestException { 6 | 7 | public UserEmailNotExistBadRequestException() { 8 | super(); 9 | this.message = "user_email_auth_fail"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/exception/UserPasswordFailBadRequestException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.exception; 2 | 3 | import flab.payment_system.common.exception.BadRequestException; 4 | 5 | public class UserPasswordFailBadRequestException extends BadRequestException { 6 | 7 | public UserPasswordFailBadRequestException() { 8 | super(); 9 | this.message = "user_password_auth_fail"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/common/exception/ServiceUnavailableException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.common.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | 5 | public class ServiceUnavailableException extends BaseException { 6 | 7 | protected ServiceUnavailableException() { 8 | this.status = String.valueOf(HttpStatus.SERVICE_UNAVAILABLE); 9 | this.code = HttpStatus.SERVICE_UNAVAILABLE.value(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/product/dto/ProductDto.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.product.dto; 2 | 3 | import jakarta.validation.constraints.Min; 4 | 5 | public record ProductDto( 6 | @Min(value = 1, message = "invalid_product_id") 7 | Long productId, 8 | String name, 9 | @Min(value = 1, message = "invalid_price") 10 | Integer price, 11 | @Min(value = 1, message = "invalid_stock") 12 | Integer stock) { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/exception/UserVerificationUnauthorizedException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.exception; 2 | 3 | import flab.payment_system.common.exception.UnauthorizedException; 4 | 5 | public class UserVerificationUnauthorizedException extends UnauthorizedException { 6 | 7 | public UserVerificationUnauthorizedException() { 8 | super(); 9 | this.message = "not_verified_email"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/exception/PaymentAlreadyApprovedConflictException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.exception; 2 | 3 | import flab.payment_system.common.exception.ConflictException; 4 | 5 | public class PaymentAlreadyApprovedConflictException extends ConflictException { 6 | public PaymentAlreadyApprovedConflictException() { 7 | super(); 8 | this.message = "payment_already_approved"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/exception/UserEmailAlreadyExistConflictException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.exception; 2 | 3 | import flab.payment_system.common.exception.ConflictException; 4 | 5 | public class UserEmailAlreadyExistConflictException extends ConflictException { 6 | 7 | public UserEmailAlreadyExistConflictException() { 8 | super(); 9 | this.message = "already_exist_user_email"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/exception/UserSignUpBadRequestException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.exception; 2 | 3 | import flab.payment_system.common.exception.BadRequestException; 4 | 5 | 6 | public class UserSignUpBadRequestException extends BadRequestException { 7 | 8 | public UserSignUpBadRequestException() { 9 | super(); 10 | this.message = "do_not_match_password_and_confirm_password"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/exception/UserVerificationIdBadRequestException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.exception; 2 | 3 | import flab.payment_system.common.exception.BadRequestException; 4 | 5 | public class UserVerificationIdBadRequestException extends BadRequestException { 6 | 7 | public UserVerificationIdBadRequestException() { 8 | super(); 9 | this.message = "not_exist_verification_id"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/toss/CashReceipt.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.toss; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class CashReceipt { 9 | 10 | private String type; 11 | private String receiptKey; 12 | private String issueNumber; 13 | private String receiptUrl; 14 | private Integer amount; 15 | private Integer taxFreeAmount; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/exception/UserVerificationEmailBadRequestException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.exception; 2 | 3 | import flab.payment_system.common.exception.BadRequestException; 4 | 5 | public class UserVerificationEmailBadRequestException extends BadRequestException { 6 | public UserVerificationEmailBadRequestException() { 7 | super(); 8 | this.message = "do_not_match_email_find_by_verification_id"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/order/repository/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.order.repository; 2 | 3 | import flab.payment_system.domain.order.entity.OrderProduct; 4 | import java.util.Optional; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface OrderRepository extends JpaRepository, OrderCustomRepository { 8 | 9 | Optional findById(Long orderId); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/enums/PaymentKakaoEndpoint.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.enums; 2 | 3 | public enum PaymentKakaoEndpoint { 4 | READY("/ready"), 5 | APPROVE("/approve"), 6 | CANCEL("/cancel"), 7 | ORDER("/order"); 8 | 9 | private final String endpoint; 10 | 11 | PaymentKakaoEndpoint(String endpoint) { 12 | this.endpoint = endpoint; 13 | } 14 | 15 | public String getEndpoint() { 16 | return endpoint; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/enums/PaymentPgCompanyStringToEnumConverter.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.enums; 2 | 3 | 4 | import org.springframework.core.convert.converter.Converter; 5 | 6 | public class PaymentPgCompanyStringToEnumConverter implements Converter { 7 | 8 | @Override 9 | public PaymentPgCompany convert(String source) { 10 | return PaymentPgCompany.valueOf(source.toUpperCase()); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/exception/PaymentKaKaoServiceUnavailableException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.exception; 2 | 3 | import flab.payment_system.common.exception.ServiceUnavailableException; 4 | 5 | public class PaymentKaKaoServiceUnavailableException extends ServiceUnavailableException { 6 | 7 | public PaymentKaKaoServiceUnavailableException() { 8 | super(); 9 | this.message = "kakao_service_unavailable"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/exception/PaymentTossServiceUnavailableException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.exception; 2 | 3 | import flab.payment_system.common.exception.ServiceUnavailableException; 4 | 5 | public class PaymentTossServiceUnavailableException extends ServiceUnavailableException { 6 | 7 | public PaymentTossServiceUnavailableException() { 8 | super(); 9 | this.message = "toss_service_unavailable"; 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/exception/UserVerificationNumberBadRequestException.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.exception; 2 | 3 | import flab.payment_system.common.exception.BadRequestException; 4 | 5 | public class UserVerificationNumberBadRequestException extends BadRequestException { 6 | 7 | public UserVerificationNumberBadRequestException() { 8 | super(); 9 | this.message = "do_not_match_verification_number_and_user_input"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/repository/UserCustomRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.repository; 2 | 3 | import com.querydsl.jpa.impl.JPAQueryFactory; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @RequiredArgsConstructor 8 | @Repository 9 | public class UserCustomRepositoryImpl implements UserCustomRepository { 10 | 11 | private final JPAQueryFactory jpaQueryFactory; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/enums/PaymentTossEndpoint.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.enums; 2 | 3 | public enum PaymentTossEndpoint { 4 | PAYMENT("/payment"), 5 | SETTLEMENT("/settlements"), 6 | APPROVE("/confirm"), 7 | CANCEL("/cancel"); 8 | 9 | private final String endpoint; 10 | 11 | PaymentTossEndpoint(String endpoint) { 12 | this.endpoint = endpoint; 13 | } 14 | 15 | public String getEndpoint() { 16 | return endpoint; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/adapter/PaymentAdapter.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.adapter; 2 | 3 | import flab.payment_system.domain.order.entity.OrderProduct; 4 | import jakarta.servlet.http.HttpSession; 5 | 6 | public interface PaymentAdapter { 7 | Long getUserId(HttpSession session); 8 | 9 | OrderProduct getOrderProductByOrderId(Long orderId); 10 | 11 | void increaseStock(Long productId, Integer quantity); 12 | 13 | void decreaseStock(Long productId, Integer quantity); 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | # [encoding-utf8] 6 | charset = utf-8 7 | 8 | # [newline-lf] 9 | end_of_line = lf 10 | 11 | # [newline-eof] 12 | insert_final_newline = true 13 | 14 | [*.bat] 15 | end_of_line = crlf 16 | 17 | [*.java] 18 | # [indentation-tab] 19 | indent_style = tab 20 | 21 | # [4-spaces-tab] 22 | indent_size = 4 23 | tab_width = 4 24 | 25 | # [no-trailing-spaces] 26 | trim_trailing_whitespace = true 27 | 28 | [line-length-120] 29 | max_line_length = 120 -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/repository/toss/TossPaymentCustomRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.repository.toss; 2 | 3 | import com.querydsl.jpa.impl.JPAQueryFactory; 4 | 5 | import lombok.RequiredArgsConstructor; 6 | 7 | import org.springframework.stereotype.Repository; 8 | 9 | @RequiredArgsConstructor 10 | @Repository 11 | public class TossPaymentCustomRepositoryImpl implements TossPaymentCustomRepository { 12 | 13 | private final JPAQueryFactory jpaQueryFactory; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/dto/UserVerifyEmailDto.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.dto; 2 | 3 | import jakarta.validation.constraints.Email; 4 | import jakarta.validation.constraints.Size; 5 | 6 | 7 | public record UserVerifyEmailDto( 8 | @Email(regexp = "^([\\w\\.\\_\\-])*[a-zA-Z0-9]+([\\w\\.\\_\\-])*([a-zA-Z0-9])+([\\w\\.\\_\\-])+@([a-zA-Z0-9]+\\.)+[a-zA-Z0-9]{2,3}$", message = "invalid_email") 9 | @Size(min = 2, max = 350, message = "invalid_email") 10 | String email 11 | ) { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/common/exception/ExceptionMessage.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.common.exception; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | @JsonInclude(JsonInclude.Include.NON_NULL) 9 | public class ExceptionMessage { 10 | 11 | private String message; 12 | private int code; 13 | 14 | @Builder 15 | ExceptionMessage(int code, String message) { 16 | this.code = code; 17 | this.message = message; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/kakao/CancelAvailableAmount.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.kakao; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 9 | public class CancelAvailableAmount { 10 | 11 | Integer total; 12 | Integer taxFree; 13 | Integer vat; 14 | Integer point; 15 | Integer discount; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/adapter/UserAdapter.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.adapter; 2 | 3 | import jakarta.servlet.http.HttpSession; 4 | 5 | import java.util.Optional; 6 | 7 | public interface UserAdapter { 8 | void sendMail(String recipient, String subject, String context); 9 | 10 | String setContextForSendValidationNumberForSendMail(String verificationNumber); 11 | 12 | Optional getUserId(HttpSession session); 13 | 14 | void setUserId(HttpSession session, Long userId); 15 | 16 | void invalidate(HttpSession session); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/kakao/CanceledAmount.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.kakao; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 9 | public class CanceledAmount { 10 | 11 | private Integer total; 12 | private Integer taxFree; 13 | private Integer vat; 14 | private Integer point; 15 | private Integer discount; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/kakao/ApprovedCancelAmount.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.kakao; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 9 | public class ApprovedCancelAmount { 10 | 11 | private Integer total; 12 | private Integer taxFree; 13 | private Integer vat; 14 | private Integer point; 15 | private Integer discount; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/toss/Cancels.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.toss; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class Cancels { 9 | 10 | private Integer cancelAmount; 11 | private String cancelReason; 12 | private Integer taxFreeAmount; 13 | private Integer taxExemptionAmount; 14 | private Integer refundableAmount; 15 | private Integer easyPayDiscountAmount; 16 | private String canceledAt; 17 | private String transactionKey; 18 | private String receiptKey; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/toss/VirtualAccount.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.toss; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class VirtualAccount { 9 | private String accountType; 10 | private String accountNumber; 11 | private String bankCode; 12 | private String customerName; 13 | private String dueDate; 14 | private String refundStatus; 15 | private boolean expired; 16 | private String settlementStatus; 17 | private RefundReceiveAccount refundReceiveAccount; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/repository/PaymentRepository.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.repository; 2 | 3 | import flab.payment_system.domain.payment.entity.Payment; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.Optional; 7 | 8 | public interface PaymentRepository extends JpaRepository, PaymentCustomRepository { 9 | Optional findByOrderProduct_OrderId(Long orderId); 10 | 11 | Optional findByOrderProduct_OrderIdAndPaymentKey(Long orderId, String paymentKey); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/config/QueryDslConfig.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.config; 2 | 3 | import com.querydsl.jpa.impl.JPAQueryFactory; 4 | import jakarta.persistence.EntityManager; 5 | import jakarta.persistence.PersistenceContext; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | public class QueryDslConfig { 11 | 12 | @PersistenceContext 13 | private EntityManager entityManager; 14 | 15 | @Bean 16 | public JPAQueryFactory jpaQueryFactory() { 17 | return new JPAQueryFactory(entityManager); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/order/dto/OrderCancelDto.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.order.dto; 2 | 3 | import jakarta.validation.constraints.Min; 4 | 5 | public record OrderCancelDto( 6 | String paymentKey, 7 | @Min(value = 1, message = "invalid_cancel_amount") 8 | Integer cancelAmount, 9 | @Min(value = 1, message = "invalid_cancel_tax_free_amount") 10 | Integer cancelTaxFreeAmount, 11 | @Min(value = 1, message = "invalid_order_id") 12 | Long orderId, 13 | @Min(value = 1, message = "invalid_product_id") 14 | Long productId, 15 | @Min(value = 1, message = "invalid_quantity") 16 | Integer quantity 17 | ) { 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/dto/UserConfirmVerificationNumberDto.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.dto; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import jakarta.validation.constraints.Email; 5 | import jakarta.validation.constraints.Size; 6 | 7 | public record UserConfirmVerificationNumberDto( 8 | Long verificationId, 9 | @Email(regexp = "^([\\w\\.\\_\\-])*[a-zA-Z0-9]+([\\w\\.\\_\\-])*([a-zA-Z0-9])+([\\w\\.\\_\\-])+@([a-zA-Z0-9]+\\.)+[a-zA-Z0-9]{2,3}$", message = "invalid_email") 10 | @Size(min = 2, max = 350, message = "invalid_email") 11 | String email, 12 | @Nonnull 13 | Integer verificationNumber 14 | ) { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/kakao/PaymentActionDetails.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.kakao; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 9 | public class PaymentActionDetails { 10 | 11 | private String aid; 12 | private String paymentActionType; 13 | private String paymentMethodType; 14 | private Integer amount; 15 | private Integer pointAmount; 16 | private Integer discountAmount; 17 | private String approvedAt; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/repository/kakao/KaKaoPaymentCustomRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.repository.kakao; 2 | 3 | import com.querydsl.jpa.impl.JPAQueryFactory; 4 | 5 | import flab.payment_system.domain.payment.entity.kakao.QKakaoPayment; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | import org.springframework.stereotype.Repository; 9 | 10 | @RequiredArgsConstructor 11 | @Repository 12 | public class KaKaoPaymentCustomRepositoryImpl implements KakaoPaymentCustomRepository { 13 | 14 | private final JPAQueryFactory jpaQueryFactory; 15 | private final QKakaoPayment kakaoPayment = QKakaoPayment.kakaoPayment; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/session/service/SessionService.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.session.service; 2 | 3 | import jakarta.servlet.http.HttpSession; 4 | import java.util.Optional; 5 | import org.springframework.stereotype.Service; 6 | 7 | @Service 8 | public class SessionService { 9 | 10 | 11 | public Optional getUserId(HttpSession session) { 12 | return Optional.ofNullable((Long) session.getAttribute("userId")); 13 | } 14 | 15 | public void setUserId(HttpSession session, Long userId) { 16 | session.setAttribute("userId", userId); 17 | } 18 | 19 | public void invalidate(HttpSession session) { 20 | session.invalidate(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/toss/Card.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.toss; 2 | 3 | 4 | import jakarta.persistence.Embeddable; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @Embeddable 11 | public class Card { 12 | 13 | private Integer amount; 14 | private String issuerCode; 15 | private String acquirerCode; 16 | private String number; 17 | private Integer installmentPlanMonths; 18 | private String approveNo; 19 | private boolean useCardPoint; 20 | private String cardType; 21 | private String ownerType; 22 | private String acquireStatus; 23 | private String isInterestFree; 24 | private String interestPayer; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/toss/CashReceipts.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.toss; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class CashReceipts { 9 | 10 | private String receiptKey; 11 | private String orderId; 12 | private String orderName; 13 | private String type; 14 | private String issueNumber; 15 | private String receiptUrl; 16 | private String businessNumber; 17 | private String transactionType; 18 | private Integer amount; 19 | private Integer taxFreeAmount; 20 | private String issueStatus; 21 | private Failure failure; 22 | private String customerIdentityNumber; 23 | private String requestedAt; 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | replay_pid* 25 | 26 | ### IntelliJ IDEA ### 27 | .idea 28 | *.iws 29 | *.iml 30 | *.ipr 31 | out/ 32 | !**/src/main/**/out/ 33 | !**/src/test/**/out/ 34 | 35 | ### NetBeans ### 36 | /nbproject/private/ 37 | /nbbuild/ 38 | /dist/ 39 | /nbdist/ 40 | /.nb-gradle/ 41 | 42 | ### VS Code ### 43 | .vscode/ 44 | 45 | .gradle/ 46 | build/ 47 | gradle/ 48 | gradlew 49 | gradlew.bat -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/dto/PaymentCreateDto.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.dto; 2 | 3 | import jakarta.validation.constraints.Min; 4 | 5 | import java.util.Optional; 6 | 7 | public record PaymentCreateDto( 8 | Long orderId, 9 | String productName, 10 | @Min(value = 1, message = "invalid_product_id") 11 | Long productId, 12 | @Min(value = 1, message = "invalid_quantity") 13 | Integer quantity, 14 | @Min(value = 1, message = "invalid_total_amount") 15 | Integer totalAmount, 16 | @Min(value = 1, message = "invalid_tax_free_amount") 17 | Integer taxFreeAmount, 18 | Integer installMonth 19 | ) { 20 | 21 | public Optional getInstallMonth() { 22 | return Optional.ofNullable(installMonth()); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/kakao/Amount.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.kakao; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import jakarta.persistence.Column; 6 | import jakarta.persistence.Embeddable; 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | 10 | @Getter 11 | @Setter 12 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 13 | @Embeddable 14 | public class Amount { 15 | 16 | private Integer total; 17 | @Column(name = "tax_free") 18 | private Integer taxFree; 19 | private Integer tax; 20 | private Integer point; 21 | private Integer discount; 22 | @Column(name = "green_deposit") 23 | private Integer greenDeposit; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/adapter/OrderAdapter.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.adapter; 2 | 3 | import flab.payment_system.domain.order.dto.OrderCancelDto; 4 | import flab.payment_system.domain.payment.enums.PaymentPgCompany; 5 | import flab.payment_system.domain.payment.response.PaymentCancelDto; 6 | import flab.payment_system.domain.product.entity.Product; 7 | import flab.payment_system.domain.user.entity.User; 8 | import jakarta.servlet.http.HttpSession; 9 | 10 | public interface OrderAdapter { 11 | void checkRemainStock(Long productId); 12 | 13 | Long getUserId(HttpSession session); 14 | 15 | PaymentCancelDto cancelPayment(OrderCancelDto orderCancelDto, PaymentPgCompany pgCompany); 16 | 17 | Product getProductByProductId(Long productId); 18 | 19 | User getUserByUserId(Long userId); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/dto/UserSignUpDto.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.dto; 2 | 3 | import jakarta.validation.constraints.Email; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.Pattern; 6 | import jakarta.validation.constraints.Size; 7 | 8 | public record UserSignUpDto( 9 | Long verificationId, 10 | @Email(regexp = "^([\\w\\.\\_\\-])*[a-zA-Z0-9]+([\\w\\.\\_\\-])*([a-zA-Z0-9])+([\\w\\.\\_\\-])+@([a-zA-Z0-9]+\\.)+[a-zA-Z0-9]{2,3}$", message = "invalid_email") 11 | @Size(min = 2, max = 350, message = "invalid_email") 12 | String email, 13 | @NotBlank(message = "invalid_password") 14 | @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*\\W).{8,16}$", message = "invalid_password") 15 | String password, 16 | String confirmPassword 17 | ) { 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/PaymentSystemApplication.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system; 2 | 3 | import jakarta.annotation.PostConstruct; 4 | import java.util.TimeZone; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.cache.annotation.EnableCaching; 9 | 10 | @EnableCaching 11 | @SpringBootApplication 12 | public class PaymentSystemApplication { 13 | 14 | @Value("${timezone}") 15 | private String timezone; 16 | 17 | @PostConstruct 18 | public void started() { 19 | TimeZone.setDefault(TimeZone.getTimeZone(timezone)); 20 | } 21 | 22 | public static void main(String[] args) { 23 | SpringApplication.run(PaymentSystemApplication.class, args); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/common/data/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.common.data; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.MappedSuperclass; 5 | import jakarta.persistence.PrePersist; 6 | import jakarta.persistence.PreUpdate; 7 | import java.time.OffsetDateTime; 8 | 9 | @MappedSuperclass 10 | public class BaseEntity { 11 | 12 | @Column(name = "created_at", updatable = false, 13 | columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP") 14 | private OffsetDateTime createdAt; 15 | 16 | @Column(name = "updated_at", columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP") 17 | private OffsetDateTime updatedAt; 18 | 19 | 20 | @PrePersist 21 | public void prePersist() { 22 | OffsetDateTime now = OffsetDateTime.now(); 23 | createdAt = now; 24 | updatedAt = now; 25 | } 26 | 27 | @PreUpdate 28 | public void preUpdate() { 29 | updatedAt = OffsetDateTime.now(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/entity/UserVerification.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.entity; 2 | 3 | 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import org.springframework.data.annotation.Id; 8 | import org.springframework.data.redis.core.RedisHash; 9 | 10 | @Getter 11 | @NoArgsConstructor 12 | @RedisHash(value = "verification", timeToLive = 1200) 13 | public class UserVerification { 14 | 15 | @Id 16 | private Long verificationId; 17 | 18 | private Integer verificationNumber; 19 | 20 | private String email; 21 | 22 | private boolean isVerified; 23 | 24 | @Builder 25 | public UserVerification(Long verificationId, String email, 26 | Integer verificationNumber, 27 | boolean isVerified) { 28 | this.verificationId = verificationId; 29 | this.email = email; 30 | this.verificationNumber = verificationNumber; 31 | this.isVerified = isVerified; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/adapter/RedissonLockAdapterImpl.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.adapter; 2 | 3 | import flab.payment_system.domain.payment.service.PaymentService; 4 | import flab.payment_system.domain.product.service.ProductService; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.stereotype.Component; 7 | 8 | @RequiredArgsConstructor 9 | @Component 10 | public class RedissonLockAdapterImpl implements RedissonLockAdapter { 11 | private final ProductService productService; 12 | private final PaymentService paymentService; 13 | 14 | @Override 15 | public void checkRemainStock(Long productId) { 16 | productService.checkRemainStock(productId); 17 | } 18 | 19 | @Override 20 | public void decreaseStock(Long productId, Integer quantity) { 21 | productService.decreaseStock(productId, quantity); 22 | } 23 | 24 | @Override 25 | public void increaseStock(Long productId, Integer quantity) { 26 | productService.increaseStock(productId, quantity); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/compensation/batch/CompensationItemWriter.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.compensation.batch; 2 | 3 | import flab.payment_system.domain.payment.entity.Payment; 4 | import jakarta.persistence.EntityManagerFactory; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.log4j.Log4j2; 7 | 8 | import org.springframework.batch.core.configuration.annotation.StepScope; 9 | import org.springframework.batch.item.database.JpaItemWriter; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | 14 | @Log4j2 15 | @Configuration 16 | @RequiredArgsConstructor 17 | public class CompensationItemWriter { 18 | 19 | @Bean 20 | @StepScope 21 | public JpaItemWriter paymentItemWriter(EntityManagerFactory entityManagerFactory) { 22 | JpaItemWriter writer = new JpaItemWriter<>(); 23 | writer.setEntityManagerFactory(entityManagerFactory); 24 | return writer; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/log/entity/AppLogs.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.log.entity; 2 | 3 | import flab.payment_system.common.data.BaseEntity; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.Lob; 10 | import jakarta.persistence.Table; 11 | import lombok.Getter; 12 | import lombok.Setter; 13 | import lombok.ToString; 14 | 15 | @Getter 16 | @Setter 17 | @ToString 18 | @Entity 19 | @Table(name="app_logs") 20 | public class AppLogs extends BaseEntity { 21 | 22 | @Id 23 | @GeneratedValue(strategy = GenerationType.IDENTITY) 24 | @Column(name = "log_id", columnDefinition = "BIGINT UNSIGNED") 25 | private Long log_id; 26 | private String trace_id; 27 | private String logger; 28 | private String log_level; 29 | 30 | @Lob 31 | private String message; 32 | private String exception; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/response/kakao/PaymentKakaoReadyDtoImpl.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.response.kakao; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import flab.payment_system.domain.payment.response.PaymentReadyDto; 6 | import lombok.Getter; 7 | 8 | @Getter 9 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 10 | public class PaymentKakaoReadyDtoImpl implements PaymentReadyDto, PaymentKakao { 11 | 12 | private String tid; 13 | private String nextRedirectAppUrl; 14 | private String nextRedirectMobileUrl; 15 | private String nextRedirectPcUrl; 16 | private String androidAppScheme; 17 | private String iosAppScheme; 18 | private String createdAt; 19 | private Long paymentId; 20 | 21 | public void setPaymentId(Long paymentId) { 22 | this.paymentId = paymentId; 23 | } 24 | 25 | public String getPaymentKey() { 26 | return this.tid; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/response/toss/Settlement.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.response.toss; 2 | 3 | import flab.payment_system.domain.payment.entity.toss.*; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | import java.io.Serial; 8 | import java.io.Serializable; 9 | 10 | @Getter 11 | @Setter 12 | public class Settlement implements Serializable { 13 | @Serial 14 | private static final long serialVersionUID = 1L; 15 | String mId; 16 | String paymentKey; 17 | String transactionKey; 18 | String orderId; 19 | String currency; 20 | String method; 21 | Integer amount; 22 | Integer interestFee; 23 | Fee[] fees; 24 | Integer supplyAmount; 25 | Integer vat; 26 | Integer payOutAmount; 27 | String approvedAt; 28 | String soldDate; 29 | String paidOutDate; 30 | Card card; 31 | EasyPay easyPay; 32 | GiftCertificate giftCertificate; 33 | MobilePhone mobilePhone; 34 | Transfer transfer; 35 | VirtualAccount virtualAccount; 36 | Cancels cancels; 37 | }; 38 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/product/repository/ProductCustomRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.product.repository; 2 | 3 | import com.querydsl.jpa.impl.JPAQueryFactory; 4 | import flab.payment_system.domain.product.entity.Product; 5 | import flab.payment_system.domain.product.entity.QProduct; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import java.util.List; 10 | 11 | @RequiredArgsConstructor 12 | @Repository 13 | public class ProductCustomRepositoryImpl implements ProductCustomRepository { 14 | 15 | private final JPAQueryFactory jpaQueryFactory; 16 | private final QProduct product = QProduct.product; 17 | 18 | @Override 19 | public List findByCursor(Long lastProductId, long size) { 20 | var query = jpaQueryFactory.selectFrom(product) 21 | .orderBy(product.productId.desc()) 22 | .limit(size); 23 | 24 | if (lastProductId != null) { 25 | query.where(product.productId.lt(lastProductId)); 26 | } 27 | 28 | return query.fetch(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/service/PaymentStrategy.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.service; 2 | 3 | import flab.payment_system.domain.order.dto.OrderCancelDto; 4 | import flab.payment_system.domain.payment.dto.PaymentCreateDto; 5 | import flab.payment_system.domain.payment.response.PaymentApprovalDto; 6 | import flab.payment_system.domain.payment.response.PaymentCancelDto; 7 | import flab.payment_system.domain.payment.response.PaymentOrderDetailDto; 8 | import flab.payment_system.domain.payment.response.PaymentReadyDto; 9 | import flab.payment_system.domain.payment.response.toss.Settlement; 10 | 11 | public interface PaymentStrategy { 12 | 13 | PaymentReadyDto createPayment(PaymentCreateDto paymentCreateDto, Long userId, String requestUrl, Long paymentId); 14 | 15 | PaymentApprovalDto approvePayment(String pgToken, Long orderId, Long userId, Long paymentId); 16 | 17 | PaymentCancelDto cancelPayment(OrderCancelDto orderCancelDto); 18 | 19 | PaymentOrderDetailDto getOrderDetail(String paymentKey); 20 | 21 | Settlement[] getSettlementList(); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.config; 2 | 3 | import flab.payment_system.common.interceptor.LoggingInterceptor; 4 | import flab.payment_system.domain.payment.enums.PaymentPgCompanyStringToEnumConverter; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.format.FormatterRegistry; 8 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 9 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 10 | 11 | @Configuration 12 | @RequiredArgsConstructor 13 | public class WebConfig implements WebMvcConfigurer { 14 | 15 | private final LoggingInterceptor loggingInterceptor; 16 | 17 | @Override 18 | public void addFormatters(FormatterRegistry registry) { 19 | registry.addConverter(new PaymentPgCompanyStringToEnumConverter()); 20 | } 21 | 22 | @Override 23 | public void addInterceptors(InterceptorRegistry registry) { 24 | registry.addInterceptor(loggingInterceptor) 25 | .order(1) 26 | .addPathPatterns("/**"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/resources/templates/mail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 인증메일 6 | 34 | 35 | 36 |
37 |

인증메일

38 |

안녕하세요. payment-system 입니다.

39 |

회원가입을 위한 인증번호 입니다.

40 |

41 |

만약 우리 사이트에 가입을 원치 않으면 해당 이메일을 무시해주세요.

42 |

좋은 하루 되세요.

43 |

from payment-system project.

44 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/entity/User.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.entity; 2 | 3 | 4 | import flab.payment_system.common.data.BaseEntity; 5 | import jakarta.persistence.Column; 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.GeneratedValue; 8 | import jakarta.persistence.GenerationType; 9 | import jakarta.persistence.Id; 10 | import jakarta.persistence.Table; 11 | import lombok.Builder; 12 | import lombok.Getter; 13 | import lombok.NoArgsConstructor; 14 | import org.checkerframework.checker.nullness.qual.NonNull; 15 | 16 | @Getter 17 | @NoArgsConstructor 18 | @Entity 19 | @Table(name = "user") 20 | public class User extends BaseEntity { 21 | 22 | @Id 23 | @GeneratedValue(strategy = GenerationType.IDENTITY) 24 | @Column(name = "user_id", columnDefinition = "BIGINT UNSIGNED") 25 | private Long userId; 26 | 27 | @NonNull 28 | @Column(name = "e-mail", columnDefinition = "VARCHAR(350)") 29 | private String email; 30 | 31 | @NonNull 32 | @Column(columnDefinition = "CHAR(60)") 33 | private String password; 34 | 35 | @Builder 36 | public User(String email, String password) { 37 | this.email = email; 38 | this.password = password; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/sonarcloud-analyze.yml: -------------------------------------------------------------------------------- 1 | name: F-Lab SonarCloud Code Analyze 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | workflow_dispatch: 7 | 8 | env: 9 | CACHED_DEPENDENCIES_PATHS: '**/node_modules' 10 | 11 | jobs: 12 | CodeAnalyze: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set SonarCloud Project Key 21 | run: | 22 | REPO_NAME=$(echo $GITHUB_REPOSITORY | cut -d '/' -f 2) 23 | ORG_NAME=$(echo $GITHUB_REPOSITORY | cut -d '/' -f 1) 24 | SONAR_PROJECT_KEY="${ORG_NAME}_${REPO_NAME}" 25 | echo "SONAR_PROJECT_KEY=$SONAR_PROJECT_KEY" >> $GITHUB_ENV 26 | 27 | - name: Analyze with SonarCloud 28 | uses: SonarSource/sonarcloud-github-action@master 29 | id: analyze-sonarcloud 30 | continue-on-error: true 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.SECRET_GITHUB_BOT }} 33 | SONAR_TOKEN: ${{ secrets.SECRET_SONARQUBE }} 34 | with: 35 | args: 36 | -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} 37 | -Dsonar.organization=f-lab-edu-1 38 | 39 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/product/controller/ProductController.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.product.controller; 2 | 3 | import flab.payment_system.domain.product.dto.ProductDto; 4 | import flab.payment_system.domain.product.service.ProductService; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | import java.util.List; 10 | 11 | /* 12 | 해당 프로젝트의 목표는 결제 시스템을 구현하는 것이기 때문에 Product 도메인은 13 | 결제 시스템 구현에 필요한 만큼만 간단히 작성 14 | */ 15 | @RestController 16 | @RequiredArgsConstructor 17 | @RequestMapping("/api/v1/product") 18 | public class ProductController { 19 | 20 | private final ProductService productService; 21 | 22 | @GetMapping 23 | public List getProductList(@RequestParam(required = false) Long lastProductId, 24 | @RequestParam(defaultValue = "5") long size) { 25 | return productService.getProductList(lastProductId, size); 26 | } 27 | 28 | @GetMapping("/{productId}") 29 | public ResponseEntity getProductDetail(@PathVariable Long productId) { 30 | ProductDto productDto = productService.getProductDetail(productId); 31 | 32 | return ResponseEntity.ok().body(productDto); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/response/kakao/PaymentKakaoApprovalDtoImpl.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.response.kakao; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import flab.payment_system.domain.payment.entity.kakao.Amount; 6 | import flab.payment_system.domain.payment.entity.kakao.CardInfo; 7 | import flab.payment_system.domain.payment.response.PaymentApprovalDto; 8 | import lombok.Getter; 9 | import lombok.Setter; 10 | 11 | @Getter 12 | @Setter 13 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 14 | public class PaymentKakaoApprovalDtoImpl implements PaymentApprovalDto, PaymentKakao { 15 | 16 | private String aid; 17 | private String tid; 18 | private String cid; 19 | private String sid; 20 | private String partnerOrderId; 21 | private String partnerUserId; 22 | private String paymentMethodType; 23 | private Amount amount; 24 | private CardInfo cardInfo; 25 | private String itemName; 26 | private String itemCode; 27 | private Integer quantity; 28 | private Integer taxFreeAmount; 29 | private Integer vatAmount; 30 | private String createdAt; 31 | private String approvedAt; 32 | private String payload; 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/response/kakao/PaymentKakaoOrderDetailDtoImpl.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.response.kakao; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import flab.payment_system.domain.payment.entity.kakao.Amount; 6 | import flab.payment_system.domain.payment.entity.kakao.CancelAvailableAmount; 7 | import flab.payment_system.domain.payment.entity.kakao.CanceledAmount; 8 | import flab.payment_system.domain.payment.entity.kakao.PaymentActionDetails; 9 | import flab.payment_system.domain.payment.response.PaymentOrderDetailDto; 10 | import lombok.Getter; 11 | 12 | @Getter 13 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 14 | public class PaymentKakaoOrderDetailDtoImpl implements PaymentOrderDetailDto, PaymentKakao { 15 | 16 | String tid; 17 | String cid; 18 | String status; 19 | String partnerOrderId; 20 | String partnerUserId; 21 | String paymentMethodType; 22 | String itemName; 23 | Integer quantity; 24 | Amount amount; 25 | CanceledAmount canceledAmount; 26 | CancelAvailableAmount cancelAvailableAmount; 27 | String createdAt; 28 | String approvedAt; 29 | String canceledAt; 30 | PaymentActionDetails[] paymentActionDetails; 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/adapter/UserAdapterImpl.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.adapter; 2 | 3 | import flab.payment_system.domain.mail.service.MailService; 4 | import flab.payment_system.domain.session.service.SessionService; 5 | import jakarta.servlet.http.HttpSession; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.Optional; 10 | 11 | @RequiredArgsConstructor 12 | @Component 13 | public class UserAdapterImpl implements UserAdapter { 14 | 15 | private final SessionService sessionService; 16 | private final MailService mailService; 17 | 18 | public void sendMail(String recipient, String subject, String context) { 19 | mailService.sendMail(recipient, subject, context); 20 | } 21 | 22 | public String setContextForSendValidationNumberForSendMail(String verificationNumber) { 23 | return mailService.setContextForSendValidationNumber(verificationNumber); 24 | } 25 | 26 | public Optional getUserId(HttpSession session) { 27 | return sessionService.getUserId(session); 28 | } 29 | 30 | public void setUserId(HttpSession session, Long userId) { 31 | sessionService.setUserId(session, userId); 32 | } 33 | 34 | public void invalidate(HttpSession session) { 35 | sessionService.invalidate(session); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/order/entity/OrderProduct.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.order.entity; 2 | 3 | import flab.payment_system.common.data.BaseEntity; 4 | import flab.payment_system.domain.product.entity.Product; 5 | import flab.payment_system.domain.user.entity.User; 6 | import jakarta.annotation.Nonnull; 7 | import jakarta.persistence.*; 8 | import lombok.Builder; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | 12 | @Getter 13 | @NoArgsConstructor 14 | @Entity 15 | @Table(name = "order_product") 16 | public class OrderProduct extends BaseEntity { 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | @Column(name = "order_id", columnDefinition = "BIGINT UNSIGNED") 20 | private Long orderId; 21 | 22 | @ManyToOne(fetch = FetchType.LAZY) 23 | @JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) 24 | private User user; 25 | 26 | @ManyToOne(fetch = FetchType.LAZY) 27 | @JoinColumn(name = "product_id", nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) 28 | private Product product; 29 | 30 | @Nonnull 31 | private Integer quantity; 32 | 33 | @Builder 34 | public OrderProduct(User user, Product product, Integer quantity) { 35 | this.user = user; 36 | this.product = product; 37 | this.quantity = quantity; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/repository/PaymentCustomRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.repository; 2 | 3 | import com.querydsl.jpa.impl.JPAQueryFactory; 4 | 5 | import flab.payment_system.domain.payment.entity.QPayment; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | import org.springframework.data.jpa.repository.Modifying; 9 | import org.springframework.stereotype.Repository; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | @RequiredArgsConstructor 13 | @Repository 14 | public class PaymentCustomRepositoryImpl implements PaymentCustomRepository { 15 | 16 | private final JPAQueryFactory jpaQueryFactory; 17 | private final QPayment payment = QPayment.payment; 18 | 19 | @Override 20 | @Transactional 21 | @Modifying(clearAutomatically = true) 22 | public long updatePaymentStateByPaymentId(Long paymentId, Integer state) { 23 | return jpaQueryFactory.update(payment) 24 | .set(payment.state, state) 25 | .where(payment.paymentId.eq(paymentId)).execute(); 26 | } 27 | 28 | @Override 29 | @Transactional 30 | @Modifying(clearAutomatically = true) 31 | public long updatePaymentStateByOrderId(Long orderId, Integer state) { 32 | return jpaQueryFactory.update(payment) 33 | .set(payment.state, state) 34 | .where(payment.orderProduct.orderId.eq(orderId)).execute(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/toss/TossPayment.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.toss; 2 | 3 | import flab.payment_system.common.data.BaseEntity; 4 | import flab.payment_system.domain.payment.entity.Payment; 5 | import jakarta.persistence.*; 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | 10 | @Getter 11 | @NoArgsConstructor 12 | @Entity 13 | @Table(name = "toss_payment") 14 | public class TossPayment extends BaseEntity { 15 | 16 | @Id 17 | @GeneratedValue(strategy = GenerationType.IDENTITY) 18 | @Column(name = "toss_payment_id", columnDefinition = "BIGINT UNSIGNED") 19 | private Long tossPaymentId; 20 | 21 | @OneToOne(fetch = FetchType.LAZY) 22 | @JoinColumn(name = "payment_id", nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) 23 | private Payment payment; 24 | @Column(columnDefinition = "VARCHAR(20)") 25 | 26 | private String type; 27 | @Column(columnDefinition = "VARCHAR(10)") 28 | 29 | private String country; 30 | @Column(columnDefinition = "VARCHAR(10)") 31 | 32 | private String currency; 33 | 34 | @Builder 35 | public TossPayment(Payment payment, String type, String country, 36 | String currency) { 37 | this.payment = payment; 38 | this.type = type; 39 | this.country = country; 40 | this.currency = currency; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/product/entity/Product.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.product.entity; 2 | 3 | import flab.payment_system.common.data.BaseEntity; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.Table; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | import org.checkerframework.checker.nullness.qual.NonNull; 13 | 14 | @Getter 15 | @NoArgsConstructor 16 | @Entity 17 | @Table(name = "product") 18 | public class Product extends BaseEntity { 19 | 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | @Column(name = "product_id", columnDefinition = "BIGINT UNSIGNED") 23 | private Long productId; 24 | 25 | @NonNull 26 | @Column(columnDefinition = "VARCHAR(20)") 27 | private String name; 28 | 29 | @NonNull 30 | @Column(columnDefinition = "INT UNSIGNED") 31 | private Integer price; 32 | 33 | @NonNull 34 | @Column(columnDefinition = "INT UNSIGNED") 35 | private Integer stock; 36 | 37 | public void setName(@NonNull String name) { 38 | this.name = name; 39 | } 40 | 41 | public void setPrice(@NonNull Integer price) { 42 | this.price = price; 43 | } 44 | 45 | public void setStock(@NonNull Integer stock) { 46 | this.stock = stock; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/kakao/KakaoPayment.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.kakao; 2 | 3 | import flab.payment_system.common.data.BaseEntity; 4 | import flab.payment_system.domain.payment.entity.Payment; 5 | import jakarta.persistence.*; 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | 10 | @Getter 11 | @NoArgsConstructor 12 | @Entity 13 | @Table(name = "kakao_payment") 14 | public class KakaoPayment extends BaseEntity { 15 | 16 | @Id 17 | @GeneratedValue(strategy = GenerationType.IDENTITY) 18 | @Column(name = "kakao_payment_id", columnDefinition = "BIGINT UNSIGNED") 19 | private Long kakaoPaymentId; 20 | 21 | @OneToOne(fetch = FetchType.LAZY) 22 | @JoinColumn(name = "payment_id", nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) 23 | private Payment payment; 24 | 25 | @Embedded 26 | private CardInfo cardInfo; 27 | 28 | @Column(name = "aid", columnDefinition = "VARCHAR(50)") 29 | private String aid; 30 | 31 | @Column(name = "payment_method_type", columnDefinition = "VARCHAR(5)") 32 | private String paymentMethodType; 33 | 34 | @Builder 35 | public KakaoPayment(Payment payment, CardInfo cardInfo, String aid, 36 | String paymentMethodType) { 37 | this.payment = payment; 38 | this.cardInfo = cardInfo; 39 | this.aid = aid; 40 | this.paymentMethodType = paymentMethodType; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/common/interceptor/LoggingInterceptor.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.common.interceptor; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.extern.log4j.Log4j2; 7 | import org.checkerframework.checker.nullness.qual.NonNull; 8 | import org.springframework.lang.Nullable; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.servlet.HandlerInterceptor; 11 | import org.springframework.web.servlet.ModelAndView; 12 | 13 | @Component 14 | @Log4j2 15 | @RequiredArgsConstructor 16 | public class LoggingInterceptor implements HandlerInterceptor { 17 | 18 | @Override 19 | public boolean preHandle(HttpServletRequest request, @NonNull HttpServletResponse response, 20 | @NonNull Object handler) { 21 | String requestURI = request.getRequestURI(); 22 | 23 | log.info("REQUEST [{}][{}]", requestURI, handler); 24 | return true; 25 | } 26 | 27 | @Override 28 | public void postHandle(HttpServletRequest request, HttpServletResponse response, 29 | @NonNull Object handler, @Nullable ModelAndView modelAndView) { 30 | String queryString = request.getQueryString(); 31 | 32 | log.info("RESPONSE [{}][{}][{}]", request.getMethod(), 33 | queryString == null ? request.getRequestURI() 34 | : request.getRequestURI() + queryString, response.getStatus()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/compensation/batch/CompensationConfig.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.compensation.batch; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.log4j.Log4j2; 5 | 6 | import org.springframework.batch.core.Job; 7 | import org.springframework.batch.core.JobParameters; 8 | import org.springframework.batch.core.JobParametersBuilder; 9 | import org.springframework.batch.core.launch.JobLauncher; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.context.annotation.Profile; 12 | import org.springframework.scheduling.annotation.EnableScheduling; 13 | import org.springframework.scheduling.annotation.Scheduled; 14 | 15 | 16 | @Log4j2 17 | @Configuration 18 | @EnableScheduling 19 | @RequiredArgsConstructor 20 | @Profile({"!test"}) 21 | public class CompensationConfig { 22 | private final JobLauncher jobLauncher; 23 | 24 | private final Job paymentSyncJob; 25 | 26 | @Scheduled(fixedDelay = 600000) 27 | public void runJob() { 28 | try { 29 | JobParameters parameters = new JobParametersBuilder() 30 | .addLong("time", System.currentTimeMillis()) 31 | .toJobParameters(); 32 | jobLauncher.run(paymentSyncJob, parameters); 33 | } catch (Exception e) { 34 | log.error("batch exception : {}", e.getMessage()); 35 | } 36 | } 37 | } 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/response/kakao/PaymentKakaoCancelDtoImpl.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.response.kakao; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import flab.payment_system.domain.payment.entity.kakao.Amount; 6 | import flab.payment_system.domain.payment.entity.kakao.ApprovedCancelAmount; 7 | import flab.payment_system.domain.payment.entity.kakao.CancelAvailableAmount; 8 | import flab.payment_system.domain.payment.entity.kakao.CanceledAmount; 9 | import flab.payment_system.domain.payment.response.PaymentCancelDto; 10 | import lombok.Getter; 11 | 12 | @Getter 13 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 14 | public class PaymentKakaoCancelDtoImpl implements PaymentCancelDto, PaymentKakao { 15 | 16 | private String cid; 17 | private String tid; 18 | private String aid; 19 | private String status; 20 | private String partnerOrderId; 21 | private String partnerUserId; 22 | private String paymentMethodType; 23 | private Amount amount; 24 | private ApprovedCancelAmount approvedCancelAmount; 25 | private CanceledAmount canceledAmount; 26 | private CancelAvailableAmount cancelAvailableAmount; 27 | private String itemName; 28 | private String itemCode; 29 | private Integer quantity; 30 | private String createdAt; 31 | private String approvedAt; 32 | private String canceledAt; 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/common/filter/SignInCheckFilter.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.common.filter; 2 | 3 | import flab.payment_system.domain.session.service.SessionService; 4 | import flab.payment_system.domain.user.exception.UserUnauthorizedException; 5 | import jakarta.servlet.FilterChain; 6 | import jakarta.servlet.ServletException; 7 | import jakarta.servlet.http.HttpServletRequest; 8 | import jakarta.servlet.http.HttpServletResponse; 9 | 10 | import java.io.IOException; 11 | import java.util.Optional; 12 | 13 | import lombok.RequiredArgsConstructor; 14 | import lombok.extern.log4j.Log4j2; 15 | import org.checkerframework.checker.nullness.qual.NonNull; 16 | import org.springframework.web.filter.OncePerRequestFilter; 17 | 18 | @Log4j2 19 | @RequiredArgsConstructor 20 | public class SignInCheckFilter extends OncePerRequestFilter { 21 | 22 | private final SessionService sessionService; 23 | 24 | @Override 25 | protected void doFilterInternal(HttpServletRequest request, 26 | @NonNull HttpServletResponse response, 27 | @NonNull FilterChain filterChain) throws ServletException, IOException { 28 | Optional.ofNullable(request.getSession(false)) 29 | .orElseThrow(UserUnauthorizedException::new); 30 | 31 | sessionService.getUserId(request.getSession(false)) 32 | .orElseThrow(UserUnauthorizedException::new); 33 | 34 | log.info("check sign in {}", request.getRequestURI()); 35 | filterChain.doFilter(request, response); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/flab/payment_system/config/DatabaseCleanUp.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.config; 2 | 3 | import com.google.common.base.CaseFormat; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.EntityManager; 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.beans.factory.InitializingBean; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.transaction.annotation.Transactional; 12 | 13 | @Service 14 | @RequiredArgsConstructor 15 | public class DatabaseCleanUp implements InitializingBean { 16 | 17 | private final EntityManager entityManager; 18 | 19 | private List tableNames; 20 | 21 | @Override 22 | public void afterPropertiesSet() { 23 | tableNames = entityManager.getMetamodel().getEntities().stream() 24 | .filter(entityType -> entityType.getJavaType().getAnnotation(Entity.class) != null) 25 | .map(entityType -> CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, entityType.getName())) 26 | .collect(Collectors.toList()); 27 | } 28 | 29 | @Transactional 30 | public void truncateAllEntity() { 31 | entityManager.flush(); 32 | entityManager.createNativeQuery("SET FOREIGN_KEY_CHECKS = 0").executeUpdate(); 33 | for (String tableName : tableNames) { 34 | entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate(); 35 | } 36 | entityManager.createNativeQuery("SET FOREIGN_KEY_CHECKS = 1").executeUpdate(); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/order/service/OrderService.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.order.service; 2 | 3 | import flab.payment_system.adapter.OrderAdapter; 4 | import flab.payment_system.domain.order.dto.OrderDto; 5 | import flab.payment_system.domain.order.dto.OrderProductDto; 6 | import flab.payment_system.domain.order.entity.OrderProduct; 7 | import flab.payment_system.domain.order.exception.OrderNotExistBadRequestException; 8 | import flab.payment_system.domain.order.repository.OrderRepository; 9 | 10 | import lombok.RequiredArgsConstructor; 11 | 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.transaction.annotation.Transactional; 14 | 15 | @RequiredArgsConstructor 16 | @Service 17 | public class OrderService { 18 | 19 | private final OrderRepository orderRepository; 20 | private final OrderAdapter orderAdapter; 21 | 22 | @Transactional 23 | public OrderDto orderProduct(OrderProductDto orderProductDto, Long userId) { 24 | orderAdapter.checkRemainStock(orderProductDto.productId()); 25 | OrderProduct orderProduct = orderRepository.save(OrderProduct.builder() 26 | .product(orderAdapter.getProductByProductId(orderProductDto.productId())) 27 | .user(orderAdapter.getUserByUserId(userId)).quantity(orderProductDto.quantity()).build()); 28 | return new OrderDto(orderProduct.getOrderId()); 29 | } 30 | 31 | public OrderProduct getOrderProductByOrderId(Long orderId) { 32 | return orderRepository.findById(orderId).orElseThrow(OrderNotExistBadRequestException::new); 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/main/resources/application-test.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | batch: 3 | job: 4 | enabled: false 5 | 6 | profiles: 7 | active: test 8 | data: 9 | redis: 10 | port: ${REDIS_PORT} 11 | host: ${REDIS_HOST} 12 | password: ${REDIS_PASSWORD} 13 | 14 | mvc: 15 | dispatch-options-request: true 16 | datasource: 17 | driver-class-name: com.mysql.cj.jdbc.Driver 18 | url: ${DB_TEST_URL}/${DB_TEST_DATABASE}?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useLegacyDatetimeCode=false 19 | username: ${DB_USER} 20 | password: ${DB_PASSWORD} 21 | jpa: 22 | show-sql: true 23 | hibernate: 24 | ddl-auto: update 25 | mail: 26 | host: ${SMTP_HOST} 27 | port: 465 28 | username: ${SMTP_ID} 29 | password: ${SMTP_PASSWORD} 30 | properties: 31 | mail: 32 | smtp: 33 | auth: true 34 | ssl: 35 | enable: true 36 | trust: ${SMTP_HOST} 37 | cache: 38 | type: redis 39 | 40 | server: 41 | shutdown: graceful 42 | 43 | logging: 44 | level: 45 | root: info 46 | 47 | timezone: ${TIMEZONE} 48 | smtp-id: ${SMTP_ID} 49 | 50 | kakao-cid: TC0ONETIME 51 | kakao-host: https://kapi.kakao.com/v1/payment 52 | kakao-adminkey: ${KAKAO_ADMIN_KEY} 53 | 54 | toss-host: https://api.tosspayments.com/v1/payments 55 | toss-client-key: ${TOSS_CLIENT_KEY} 56 | toss-secret-key: ${TOSS_SECRET_KEY} 57 | test-url: ${TEST_URL} 58 | compensation-path: ${COMPENSATION_PATH} 59 | test-file-path: ${TEST_FILE_PATH} 60 | target-path: ${TARGET_PATH} 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/config/PaymentStrategyConfig.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.config; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.Lazy; 10 | 11 | import flab.payment_system.domain.payment.enums.PaymentPgCompany; 12 | import flab.payment_system.domain.payment.service.PaymentStrategy; 13 | import flab.payment_system.domain.payment.service.kakao.PaymentStrategyKaKaoService; 14 | import flab.payment_system.domain.payment.service.toss.PaymentStrategyTossService; 15 | 16 | @Configuration 17 | public class PaymentStrategyConfig { 18 | 19 | private final PaymentStrategyKaKaoService paymentStrategyKaKaoService; 20 | 21 | private final PaymentStrategyTossService paymentStrategyTossService; 22 | 23 | @Autowired 24 | public PaymentStrategyConfig(@Lazy PaymentStrategyKaKaoService paymentStrategyKaKaoService, 25 | @Lazy PaymentStrategyTossService paymentStrategyTossService) { 26 | this.paymentStrategyKaKaoService = paymentStrategyKaKaoService; 27 | this.paymentStrategyTossService = paymentStrategyTossService; 28 | 29 | } 30 | 31 | @Bean 32 | public Map paymentStrategies() { 33 | Map paymentStrategies = new HashMap<>(); 34 | paymentStrategies.put(PaymentPgCompany.KAKAO, paymentStrategyKaKaoService); 35 | paymentStrategies.put(PaymentPgCompany.TOSS, paymentStrategyTossService); 36 | return paymentStrategies; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | active: prod 4 | data: 5 | redis: 6 | port: ${REDIS_PORT} 7 | host: ${REDIS_HOST} 8 | password: ${REDIS_PASSWORD} 9 | 10 | mvc: 11 | dispatch-options-request: true 12 | datasource: 13 | driver-class-name: com.mysql.cj.jdbc.Driver 14 | # 다국적 서비스 사용 대비를 위해 database(mysql) server timezone 은 utc, 애플리케이션 단에서 kst OffsetDateTime or ZoneDateTime 타입 사용 15 | url: ${DB_URL}/${DB_DATABASE}?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useLegacyDatetimeCode=false 16 | username: ${DB_USER} 17 | password: ${DB_PASSWORD} 18 | jpa: 19 | show-sql: true 20 | hibernate: 21 | ddl-auto: update 22 | mail: 23 | host: ${SMTP_HOST} 24 | port: 465 25 | username: ${SMTP_ID} 26 | password: ${SMTP_PASSWORD} 27 | properties: 28 | mail: 29 | smtp: 30 | auth: true 31 | ssl: 32 | enable: true 33 | trust: ${SMTP_HOST} 34 | cache: 35 | type: redis 36 | batch: 37 | jdbc: 38 | initialize-schema: always 39 | job: 40 | enabled: false 41 | 42 | 43 | server: 44 | shutdown: graceful 45 | 46 | logging: 47 | level: 48 | root: info 49 | config: classpath:log4j2-${spring.profiles.active}.xml 50 | 51 | timezone: ${TIMEZONE} 52 | smtp-id: ${SMTP_ID} 53 | 54 | kakao-cid: TC0ONETIME 55 | kakao-host: https://kapi.kakao.com/v1/payment 56 | kakao-adminkey: ${KAKAO_ADMIN_KEY} 57 | 58 | toss-host: https://api.tosspayments.com/v1 59 | toss-client-key: ${TOSS_CLIENT_KEY} 60 | toss-secret-key: ${TOSS_SECRET_KEY} 61 | compensation-path: ${COMPENSATION_PATH} 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/main/resources/log4j2-prod.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/adapter/PaymentAdapterImpl.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.adapter; 2 | 3 | import flab.payment_system.domain.order.entity.OrderProduct; 4 | import flab.payment_system.domain.order.service.OrderService; 5 | import flab.payment_system.domain.redisson.service.RedissonLockService; 6 | import flab.payment_system.domain.user.service.UserService; 7 | import jakarta.servlet.http.HttpSession; 8 | 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.context.annotation.Lazy; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Component 14 | public class PaymentAdapterImpl implements PaymentAdapter { 15 | 16 | private final UserService userService; 17 | private final OrderService orderService; 18 | private final RedissonLockService redissonLockService; 19 | 20 | @Autowired 21 | public PaymentAdapterImpl(UserService userService, @Lazy OrderService orderService, 22 | @Lazy RedissonLockService redissonLockService) { 23 | this.userService = userService; 24 | this.orderService = orderService; 25 | this.redissonLockService = redissonLockService; 26 | } 27 | 28 | @Override 29 | public Long getUserId(HttpSession session) { 30 | return userService.getUserId(session); 31 | } 32 | 33 | @Override 34 | public OrderProduct getOrderProductByOrderId(Long orderId) { 35 | return orderService.getOrderProductByOrderId(orderId); 36 | } 37 | 38 | @Override 39 | public void increaseStock(Long productId, Integer quantity) { 40 | redissonLockService.increaseStock(productId, quantity); 41 | } 42 | 43 | @Override 44 | public void decreaseStock(Long productId, Integer quantity) { 45 | redissonLockService.decreaseStock(productId, quantity); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/adapter/OrderAdapterImpl.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.adapter; 2 | 3 | import flab.payment_system.domain.order.dto.OrderCancelDto; 4 | import flab.payment_system.domain.payment.enums.PaymentPgCompany; 5 | import flab.payment_system.domain.payment.response.PaymentCancelDto; 6 | import flab.payment_system.domain.payment.service.PaymentService; 7 | import flab.payment_system.domain.product.entity.Product; 8 | import flab.payment_system.domain.product.service.ProductService; 9 | import flab.payment_system.domain.user.entity.User; 10 | import flab.payment_system.domain.user.service.UserService; 11 | import jakarta.servlet.http.HttpSession; 12 | import lombok.RequiredArgsConstructor; 13 | 14 | import org.springframework.stereotype.Component; 15 | 16 | @Component 17 | @RequiredArgsConstructor 18 | public class OrderAdapterImpl implements OrderAdapter { 19 | private final UserService userService; 20 | private final ProductService productService; 21 | private final PaymentService paymentService; 22 | 23 | @Override 24 | public void checkRemainStock(Long productId) { 25 | productService.checkRemainStock(productId); 26 | } 27 | 28 | @Override 29 | public Long getUserId(HttpSession session) { 30 | return userService.getUserId(session); 31 | } 32 | 33 | @Override 34 | public PaymentCancelDto cancelPayment(OrderCancelDto orderCancelDto, PaymentPgCompany pgCompany) { 35 | return paymentService.cancelPayment(orderCancelDto, pgCompany); 36 | } 37 | 38 | @Override 39 | public Product getProductByProductId(Long productId) { 40 | return productService.getProductByProductId(productId); 41 | } 42 | 43 | @Override 44 | public User getUserByUserId(Long userId) { 45 | return userService.getUserByUserId(userId); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/redisson/service/RedissonLockService.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.redisson.service; 2 | 3 | import flab.payment_system.adapter.RedissonLockAdapter; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import lombok.RequiredArgsConstructor; 8 | import org.redisson.api.RLock; 9 | import org.redisson.api.RedissonClient; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.transaction.annotation.Transactional; 12 | 13 | @Service 14 | @RequiredArgsConstructor 15 | public class RedissonLockService { 16 | 17 | private final RedissonClient redissonClient; 18 | private final RedissonLockAdapter redissonLockAdapter; 19 | 20 | @Transactional 21 | public void decreaseStock(Long productId, Integer quantity) { 22 | RLock lock = redissonClient.getLock(String.valueOf(productId)); 23 | 24 | try { 25 | boolean available = lock.tryLock(10, 1, TimeUnit.SECONDS); 26 | 27 | if (!available) { 28 | throw new RuntimeException(); 29 | } 30 | 31 | redissonLockAdapter.checkRemainStock(productId); 32 | redissonLockAdapter.decreaseStock(productId, quantity); 33 | 34 | } catch (InterruptedException e) { 35 | throw new RuntimeException(e); 36 | } finally { 37 | lock.unlock(); 38 | } 39 | } 40 | 41 | @Transactional 42 | public void increaseStock(Long productId, Integer quantity) { 43 | RLock lock = redissonClient.getLock(String.valueOf(productId)); 44 | 45 | try { 46 | boolean available = lock.tryLock(10, 1, TimeUnit.SECONDS); 47 | 48 | if (!available) { 49 | throw new RuntimeException(); 50 | } 51 | 52 | redissonLockAdapter.checkRemainStock(productId); 53 | redissonLockAdapter.increaseStock(productId, quantity); 54 | 55 | } catch (InterruptedException e) { 56 | throw new RuntimeException(e); 57 | } finally { 58 | lock.unlock(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/order/controller/OrderController.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.order.controller; 2 | 3 | import flab.payment_system.adapter.OrderAdapter; 4 | import flab.payment_system.domain.order.dto.OrderCancelDto; 5 | import flab.payment_system.domain.order.dto.OrderDto; 6 | import flab.payment_system.domain.order.dto.OrderProductDto; 7 | import flab.payment_system.domain.order.service.OrderService; 8 | import flab.payment_system.domain.payment.enums.PaymentPgCompany; 9 | import flab.payment_system.domain.payment.response.PaymentCancelDto; 10 | import jakarta.servlet.http.HttpSession; 11 | import jakarta.validation.Valid; 12 | import lombok.RequiredArgsConstructor; 13 | 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.web.bind.annotation.PathVariable; 16 | import org.springframework.web.bind.annotation.PostMapping; 17 | import org.springframework.web.bind.annotation.RequestBody; 18 | import org.springframework.web.bind.annotation.RequestMapping; 19 | import org.springframework.web.bind.annotation.RestController; 20 | 21 | @RestController 22 | @RequiredArgsConstructor 23 | @RequestMapping("/api/v1/order") 24 | public class OrderController { 25 | 26 | private final OrderService orderService; 27 | private final OrderAdapter orderAdapter; 28 | 29 | @PostMapping 30 | public ResponseEntity orderProductRequest( 31 | @RequestBody @Valid OrderProductDto orderProductDto, HttpSession session) { 32 | Long userId = orderAdapter.getUserId(session); 33 | OrderDto orderDto = orderService.orderProduct(orderProductDto, userId); 34 | 35 | return ResponseEntity.ok().body(orderDto); 36 | } 37 | 38 | @PostMapping("/{pgCompany}/cancel") 39 | public ResponseEntity orderCancel( 40 | @PathVariable PaymentPgCompany pgCompany, 41 | @RequestBody @Valid OrderCancelDto orderCancelDto) { 42 | PaymentCancelDto paymentCancelDto = orderAdapter.cancelPayment(orderCancelDto, pgCompany); 43 | 44 | return ResponseEntity.ok().body(paymentCancelDto); 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/kakao/CardInfo.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity.kakao; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import jakarta.persistence.Column; 6 | import jakarta.persistence.Embeddable; 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | 10 | @Getter 11 | @Setter 12 | @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 13 | @Embeddable 14 | public class CardInfo { 15 | 16 | @Column(name = "interest_free_install", columnDefinition = "VARCHAR(5)") 17 | private String interestFreeInstall; 18 | @Column(columnDefinition = "VARCHAR(10)") 19 | private String bin; 20 | @Column(name = "card_type", columnDefinition = "VARCHAR(10)") 21 | private String cardType; 22 | @Column(name = "card_mid", columnDefinition = "VARCHAR(20)") 23 | private String cardMid; 24 | @Column(name = "approved_id", columnDefinition = "VARCHAR(20)") 25 | private String approvedId; 26 | @Column(name = "install_month", columnDefinition = "VARCHAR(5)") 27 | private String installMonth; 28 | @Column(name = "purchase_corp", columnDefinition = "VARCHAR(10)") 29 | private String purchaseCorp; 30 | @Column(name = "purchase_corp_code", columnDefinition = "VARCHAR(10)") 31 | private String purchaseCorpCode; 32 | @Column(name = "issure_corp", columnDefinition = "VARCHAR(10)") 33 | private String issuerCorp; 34 | @Column(name = "issuer_corp_code", columnDefinition = "VARCHAR(10)") 35 | private String issuerCorpCode; 36 | @Column(name = "kakaopay_purchase_corp", columnDefinition = "VARCHAR(10)") 37 | private String kakaopayPurchaseCorp; 38 | @Column(name = "kakaopay_purchase_corp_code", columnDefinition = "VARCHAR(10)") 39 | private String kakaopayPurchaseCorpCode; 40 | @Column(name = "kakaopay_issuer_corp", columnDefinition = "VARCHAR(10)") 41 | private String kakaopayIssuerCorp; 42 | @Column(name = "kakaopay_issuer_corp_code", columnDefinition = "VARCHAR(10)") 43 | private String kakaopayIssuerCorpCode; 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/mail/service/MailService.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.mail.service; 2 | 3 | import flab.payment_system.domain.user.exception.UserVerifyUserEmailException; 4 | import jakarta.mail.MessagingException; 5 | import jakarta.mail.internet.InternetAddress; 6 | import jakarta.mail.internet.MimeMessage; 7 | import java.io.UnsupportedEncodingException; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.beans.factory.annotation.Value; 10 | import org.springframework.mail.javamail.JavaMailSender; 11 | import org.springframework.stereotype.Service; 12 | import org.thymeleaf.context.Context; 13 | import org.thymeleaf.spring6.SpringTemplateEngine; 14 | 15 | @Service 16 | public class MailService { 17 | 18 | private final JavaMailSender javaMailSender; 19 | private final SpringTemplateEngine templateEngine; 20 | 21 | private final String smtpId; 22 | 23 | @Autowired 24 | MailService(@Value("${smtp-id}") String smtpId, JavaMailSender javaMailSender, 25 | SpringTemplateEngine templateEngine) { 26 | this.javaMailSender = javaMailSender; 27 | this.templateEngine = templateEngine; 28 | this.smtpId = smtpId; 29 | 30 | } 31 | 32 | 33 | public void sendMail(String recipient, String subject, 34 | String context) { 35 | try { 36 | 37 | MimeMessage message = javaMailSender.createMimeMessage(); 38 | 39 | message.addRecipients(MimeMessage.RecipientType.TO, recipient); 40 | message.setSubject(subject); 41 | message.setText(context, 42 | "utf-8", 43 | "html"); 44 | message.setFrom(new InternetAddress(smtpId, 45 | "ps project")); 46 | 47 | javaMailSender.send(message); 48 | } catch (MessagingException | UnsupportedEncodingException exception) { 49 | throw new UserVerifyUserEmailException(); 50 | } 51 | } 52 | 53 | public String setContextForSendValidationNumber(String verificationNumber) { 54 | Context context = new Context(); 55 | context.setVariable("verificationNumber", verificationNumber); 56 | return templateEngine.process("mail", context); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/resources/log4j2-local.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %traceId | %style{%d{ISO8601}}{green} %highlight{%-5level 6 | }[%style{%t}{bright,blue}] %style{%C{1.}}{bright,yellow}: %msg%n%throwable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/entity/Payment.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.entity; 2 | 3 | import java.io.Serial; 4 | import java.io.Serializable; 5 | 6 | import flab.payment_system.common.data.BaseEntity; 7 | import flab.payment_system.domain.order.entity.OrderProduct; 8 | import jakarta.annotation.Nonnull; 9 | import jakarta.persistence.*; 10 | import lombok.Builder; 11 | import lombok.Getter; 12 | import lombok.NoArgsConstructor; 13 | import lombok.Setter; 14 | 15 | import org.checkerframework.checker.nullness.qual.NonNull; 16 | 17 | @Getter 18 | @NoArgsConstructor 19 | @Entity 20 | @Table(name = "payment") 21 | public class Payment extends BaseEntity implements Serializable { 22 | @Serial 23 | private static final long serialVersionUID = 1L; 24 | @Id 25 | @GeneratedValue(strategy = GenerationType.IDENTITY) 26 | @Column(name = "payment_id", columnDefinition = "BIGINT UNSIGNED") 27 | private Long paymentId; 28 | 29 | @OneToOne(fetch = FetchType.LAZY) 30 | @JoinColumn(name = "order_id", nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), unique = true) 31 | private OrderProduct orderProduct; 32 | 33 | @Nonnull 34 | @Column(columnDefinition = "TINYINT UNSIGNED") 35 | private Integer state; 36 | 37 | // 0 카카오, 1 토스 38 | @NonNull 39 | @Column(name = "pgCompany", columnDefinition = "TINYINT UNSIGNED") 40 | private Integer pgCompany; 41 | 42 | @Nonnull 43 | private Integer totalAmount; 44 | 45 | @Nonnull 46 | private Integer taxFreeAmount; 47 | 48 | @Nonnull 49 | private Integer installMonth; 50 | 51 | @Column(columnDefinition = "VARCHAR(200)") 52 | private String paymentKey; 53 | 54 | @Builder 55 | public Payment(OrderProduct orderProduct, Integer state, Integer pgCompany, Integer totalAmount, 56 | Integer taxFreeAmount, Integer installMonth, String paymentKey) { 57 | this.orderProduct = orderProduct; 58 | this.state = state; 59 | this.pgCompany = pgCompany; 60 | this.totalAmount = totalAmount; 61 | this.taxFreeAmount = taxFreeAmount; 62 | this.installMonth = installMonth; 63 | this.paymentKey = paymentKey; 64 | } 65 | 66 | public void setPaymentKey(String paymentKey) { 67 | this.paymentKey = paymentKey; 68 | } 69 | 70 | public void setState(Integer state) { 71 | this.state = state; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/compensation/batch/CompensationItemProcessor.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.compensation.batch; 2 | 3 | import java.util.*; 4 | 5 | import flab.payment_system.domain.payment.entity.Payment; 6 | import flab.payment_system.domain.payment.enums.PaymentStateConstant; 7 | import flab.payment_system.domain.payment.repository.PaymentRepository; 8 | import flab.payment_system.domain.payment.response.toss.Settlement; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.log4j.Log4j2; 11 | 12 | import org.springframework.batch.core.configuration.annotation.StepScope; 13 | import org.springframework.batch.item.ItemProcessor; 14 | import org.springframework.context.annotation.Bean; 15 | import org.springframework.context.annotation.Configuration; 16 | 17 | @Log4j2 18 | @Configuration 19 | @RequiredArgsConstructor 20 | public class CompensationItemProcessor { 21 | private final PaymentRepository paymentRepository; 22 | 23 | @Bean 24 | @StepScope 25 | public ItemProcessor paymentSettlementItemProcessor() { 26 | return settlement -> processPaymentFromSettlement(settlement); 27 | } 28 | 29 | private Payment processPaymentFromSettlement(Settlement settlement) { 30 | 31 | Optional optionalPayment = paymentRepository.findByOrderProduct_OrderIdAndPaymentKey( 32 | Long.valueOf(settlement.getOrderId()), settlement.getPaymentKey()); 33 | 34 | if (optionalPayment.isPresent()) { 35 | Payment payment = optionalPayment.get(); 36 | updatePaymentState(payment); // Settlement 정보가 있으므로 무조건 APPROVED 처리 37 | return payment; 38 | } else { 39 | // 일치하는 Payment가 없는 경우 경고 로그 출력 40 | log.warn("No matching payment found for orderId: {}, paymentKey: {}", settlement.getOrderId(), 41 | settlement.getPaymentKey()); 42 | return null; 43 | } 44 | } 45 | 46 | private void updatePaymentState(Payment payment) { 47 | Integer dbState = payment.getState(); 48 | 49 | if (!dbState.equals(PaymentStateConstant.APPROVED.getValue())) { 50 | payment.setState(PaymentStateConstant.APPROVED.getValue()); 51 | paymentRepository.save(payment); 52 | log.info("Payment state updated to APPROVED for paymentKey: {}", payment.getPaymentKey()); 53 | } else { 54 | log.info("Payment already APPROVED for paymentKey: {}", payment.getPaymentKey()); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/client/toss/PaymentTossClient.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.client.toss; 2 | 3 | import flab.payment_system.domain.payment.exception.PaymentTossServiceUnavailableException; 4 | import flab.payment_system.domain.payment.response.PaymentCancelDto; 5 | import flab.payment_system.domain.payment.response.PaymentOrderDetailDto; 6 | import flab.payment_system.domain.payment.response.PaymentReadyDto; 7 | import flab.payment_system.domain.payment.response.toss.PaymentTossDtoImpl; 8 | 9 | import java.util.Map; 10 | import java.util.Optional; 11 | 12 | import flab.payment_system.domain.payment.response.toss.Settlement; 13 | import lombok.RequiredArgsConstructor; 14 | import org.springframework.http.HttpEntity; 15 | import org.springframework.http.HttpMethod; 16 | import org.springframework.http.ResponseEntity; 17 | import org.springframework.stereotype.Component; 18 | import org.springframework.web.client.RestTemplate; 19 | 20 | @RequiredArgsConstructor 21 | @Component 22 | public class PaymentTossClient { 23 | 24 | private final RestTemplate restTemplate; 25 | 26 | 27 | public PaymentReadyDto createPayment(String url, HttpEntity> body) { 28 | return Optional.ofNullable(restTemplate.postForObject(url, 29 | body, PaymentTossDtoImpl.class)).orElseThrow( 30 | PaymentTossServiceUnavailableException::new); 31 | } 32 | 33 | public PaymentTossDtoImpl approvePayment(String url, HttpEntity> body) { 34 | return Optional.ofNullable( 35 | restTemplate.postForObject(url, 36 | body, PaymentTossDtoImpl.class)).orElseThrow( 37 | PaymentTossServiceUnavailableException::new); 38 | } 39 | 40 | public PaymentCancelDto cancelPayment(String url, HttpEntity> body) { 41 | return Optional.ofNullable( 42 | restTemplate.postForObject(url, 43 | body, PaymentTossDtoImpl.class)).orElseThrow( 44 | PaymentTossServiceUnavailableException::new); 45 | } 46 | 47 | public PaymentOrderDetailDto getOrderDetail(String url, HttpEntity requestEntity) { 48 | ResponseEntity response = restTemplate.exchange( 49 | url, HttpMethod.GET, requestEntity, PaymentTossDtoImpl.class); 50 | return response.getBody(); 51 | } 52 | 53 | public Settlement[] getSettlementList(String url, HttpEntity requestEntity) { 54 | ResponseEntity response = restTemplate.exchange( 55 | url, HttpMethod.GET, requestEntity, Settlement[].class); 56 | return response.getBody(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/client/kakao/PaymentKakaoClient.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.client.kakao; 2 | 3 | import flab.payment_system.domain.payment.exception.PaymentKaKaoServiceUnavailableException; 4 | import flab.payment_system.domain.payment.response.PaymentCancelDto; 5 | import flab.payment_system.domain.payment.response.PaymentOrderDetailDto; 6 | import flab.payment_system.domain.payment.response.PaymentReadyDto; 7 | import flab.payment_system.domain.payment.response.kakao.PaymentKakaoApprovalDtoImpl; 8 | import flab.payment_system.domain.payment.response.kakao.PaymentKakaoCancelDtoImpl; 9 | import flab.payment_system.domain.payment.response.kakao.PaymentKakaoOrderDetailDtoImpl; 10 | import flab.payment_system.domain.payment.response.kakao.PaymentKakaoReadyDtoImpl; 11 | import java.util.Optional; 12 | import lombok.RequiredArgsConstructor; 13 | import org.springframework.http.HttpEntity; 14 | import org.springframework.stereotype.Component; 15 | import org.springframework.util.MultiValueMap; 16 | import org.springframework.web.client.RestTemplate; 17 | 18 | @RequiredArgsConstructor 19 | @Component 20 | public class PaymentKakaoClient { 21 | 22 | private final RestTemplate restTemplate; 23 | 24 | 25 | public PaymentReadyDto createPayment(String url, 26 | HttpEntity> body) { 27 | 28 | return Optional.ofNullable( 29 | restTemplate.postForObject(url, 30 | body, PaymentKakaoReadyDtoImpl.class)).orElseThrow( 31 | PaymentKaKaoServiceUnavailableException::new); 32 | } 33 | 34 | 35 | public PaymentKakaoApprovalDtoImpl approvePayment(String url, 36 | HttpEntity> body) { 37 | 38 | return Optional.ofNullable( 39 | restTemplate.postForObject(url, 40 | body, 41 | PaymentKakaoApprovalDtoImpl.class)) 42 | .orElseThrow(PaymentKaKaoServiceUnavailableException::new); 43 | } 44 | 45 | public PaymentCancelDto cancelPayment(String url, 46 | HttpEntity> body) { 47 | return Optional.ofNullable( 48 | restTemplate.postForObject(url, 49 | body, 50 | PaymentKakaoCancelDtoImpl.class)) 51 | .orElseThrow(PaymentKaKaoServiceUnavailableException::new); 52 | } 53 | 54 | public PaymentOrderDetailDto getOrderDetail(String url, 55 | HttpEntity> body) { 56 | 57 | return Optional.ofNullable( 58 | restTemplate.postForObject(url, 59 | body, 60 | PaymentKakaoOrderDetailDtoImpl.class)) 61 | .orElseThrow(PaymentKaKaoServiceUnavailableException::new); 62 | } 63 | 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.controller; 2 | 3 | import flab.payment_system.domain.user.dto.UserConfirmVerificationNumberDto; 4 | import flab.payment_system.domain.user.dto.UserDto; 5 | import flab.payment_system.domain.user.dto.UserSignUpDto; 6 | import flab.payment_system.domain.user.dto.UserVerificationDto; 7 | import flab.payment_system.domain.user.dto.UserVerifyEmailDto; 8 | import flab.payment_system.domain.user.service.UserService; 9 | import jakarta.servlet.http.HttpSession; 10 | import jakarta.validation.Valid; 11 | import java.net.URI; 12 | import lombok.RequiredArgsConstructor; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.web.bind.annotation.PostMapping; 15 | import org.springframework.web.bind.annotation.RequestBody; 16 | import org.springframework.web.bind.annotation.RequestMapping; 17 | import org.springframework.web.bind.annotation.RestController; 18 | 19 | @RestController 20 | @RequiredArgsConstructor 21 | @RequestMapping("/api/v1/users") 22 | public class UserController { 23 | 24 | private final UserService userService; 25 | 26 | @PostMapping("/sign-up") 27 | public ResponseEntity signUpUser(@RequestBody @Valid UserSignUpDto userSignUpDto) { 28 | 29 | userService.signUpUser(userSignUpDto); 30 | 31 | return ResponseEntity.created(URI.create("/users/sign-up")).build(); 32 | } 33 | 34 | @PostMapping("/email") 35 | public ResponseEntity verifyUserEmail( 36 | @RequestBody @Valid UserVerifyEmailDto userVerifyEmailDto) { 37 | 38 | UserVerificationDto userVerificationDto = userService.verifyUserEmail(userVerifyEmailDto); 39 | 40 | return ResponseEntity.ok().body(userVerificationDto); 41 | } 42 | 43 | @PostMapping("/email/verification-number") 44 | public ResponseEntity confirmVerificationNumber( 45 | @RequestBody @Valid UserConfirmVerificationNumberDto userConfirmVerificationNumberDto) { 46 | 47 | userService.confirmVerificationNumber(userConfirmVerificationNumberDto); 48 | 49 | return ResponseEntity.noContent().build(); 50 | } 51 | 52 | 53 | @PostMapping("/sign-in") 54 | public ResponseEntity signInUser(@RequestBody UserDto userDto, HttpSession session) { 55 | 56 | userService.signInUser(userDto, session); 57 | 58 | return ResponseEntity.noContent().build(); 59 | } 60 | 61 | @PostMapping("/sign-out") 62 | public ResponseEntity signOutUser(HttpSession session) { 63 | 64 | userService.signOutUser(session); 65 | 66 | return ResponseEntity.noContent().build(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/common/filter/ExceptionHandlerFilter.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.common.filter; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import flab.payment_system.common.exception.BaseException; 5 | import flab.payment_system.common.exception.ExceptionMessage; 6 | import jakarta.servlet.FilterChain; 7 | import jakarta.servlet.http.HttpServletRequest; 8 | import jakarta.servlet.http.HttpServletResponse; 9 | import java.io.IOException; 10 | import lombok.extern.log4j.Log4j2; 11 | import org.checkerframework.checker.nullness.qual.NonNull; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.web.filter.OncePerRequestFilter; 14 | 15 | @Log4j2 16 | public class ExceptionHandlerFilter extends OncePerRequestFilter { 17 | 18 | @Override 19 | protected void doFilterInternal( 20 | @NonNull HttpServletRequest request, 21 | @NonNull HttpServletResponse response, 22 | @NonNull FilterChain filterChain 23 | ) throws IOException { 24 | try { 25 | filterChain.doFilter(request, response); 26 | } catch (BaseException baseException) { 27 | handleBaseException(baseException, response); 28 | } catch (Exception exception) { 29 | handleException(exception, response); 30 | } 31 | } 32 | 33 | private void handleBaseException(BaseException baseException, 34 | HttpServletResponse response) throws IOException { 35 | ExceptionMessage exceptionMessage = ExceptionMessage.builder() 36 | .message(baseException.getStatus() + " : " + baseException.getMessage()) 37 | .code(baseException.getCode()).build(); 38 | 39 | byte[] responseToSend = restResponseBytes(exceptionMessage); 40 | 41 | response.setHeader("Content-Type", "application/json"); 42 | response.setStatus(exceptionMessage.getCode()); 43 | response.getOutputStream().write(responseToSend); 44 | 45 | log.info(baseException.getStatus() + " : " + baseException.getMessage(), baseException); 46 | } 47 | 48 | private void handleException(Exception exception, 49 | HttpServletResponse response) throws IOException { 50 | ExceptionMessage exceptionMessage = ExceptionMessage.builder() 51 | .message(HttpStatus.INTERNAL_SERVER_ERROR + " : " + exception.getMessage()) 52 | .code(HttpStatus.INTERNAL_SERVER_ERROR.value()).build(); 53 | 54 | byte[] responseToSend = restResponseBytes(exceptionMessage); 55 | 56 | response.setHeader("Content-Type", "application/json"); 57 | response.setStatus(exceptionMessage.getCode()); 58 | response.getOutputStream().write(responseToSend); 59 | 60 | log.error(HttpStatus.INTERNAL_SERVER_ERROR + " : " + exception.getMessage(), exception); 61 | } 62 | 63 | private byte[] restResponseBytes(ExceptionMessage exceptionMessage) 64 | throws IOException { 65 | String serialized = new ObjectMapper().writeValueAsString(exceptionMessage); 66 | return serialized.getBytes(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/product/service/ProductService.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.product.service; 2 | 3 | import flab.payment_system.domain.product.entity.Product; 4 | import flab.payment_system.domain.product.dto.ProductDto; 5 | import flab.payment_system.domain.product.exception.ProductNotExistBadRequestException; 6 | import flab.payment_system.domain.product.exception.ProductSoldOutException; 7 | import flab.payment_system.domain.product.repository.ProductRepository; 8 | import org.springframework.cache.annotation.Cacheable; 9 | import org.springframework.transaction.annotation.Transactional; 10 | import lombok.RequiredArgsConstructor; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.util.List; 14 | import java.util.stream.Collectors; 15 | 16 | @Service 17 | @RequiredArgsConstructor 18 | @Transactional(readOnly = true) 19 | public class ProductService { 20 | 21 | private final ProductRepository productRepository; 22 | 23 | @Cacheable(value = "productListCache", key = "#root.methodName + '_' + #lastProductId + '_' + #size", cacheManager = "cacheManager") 24 | public List getProductList(Long lastProductId, long size) { 25 | List products = productRepository.findByCursor(lastProductId, size); 26 | return products.stream() 27 | .map(product -> new ProductDto(product.getProductId(), product.getName(), product.getPrice(), product.getStock())) 28 | .collect(Collectors.toList()); 29 | } 30 | 31 | public ProductDto getProductDetail(Long productId) { 32 | Product product = productRepository.findById(productId).orElseThrow( 33 | ProductNotExistBadRequestException::new); 34 | return new ProductDto(product.getProductId(), product.getName(), product.getPrice(), 35 | product.getStock()); 36 | } 37 | 38 | public void checkRemainStock(Long productId) { 39 | Product product = productRepository.findById(productId).orElseThrow( 40 | ProductNotExistBadRequestException::new); 41 | if (product.getStock() == 0) { 42 | throw new ProductSoldOutException(); 43 | } 44 | } 45 | 46 | @Transactional 47 | public void decreaseStock(Long productId, Integer quantity) { 48 | Product product = productRepository.findById(productId).orElseThrow( 49 | ProductNotExistBadRequestException::new); 50 | if (product.getStock() - quantity < 0) { 51 | throw new ProductSoldOutException(); 52 | } 53 | product.setStock(product.getStock() - quantity); 54 | } 55 | 56 | @Transactional 57 | public void increaseStock(Long productId, Integer quantity) { 58 | Product product = productRepository.findById(productId).orElseThrow( 59 | ProductNotExistBadRequestException::new); 60 | product.setStock(product.getStock() + quantity); 61 | } 62 | 63 | public Product getProductByProductId(Long productId) { 64 | return productRepository.findById(productId).orElseThrow( 65 | ProductNotExistBadRequestException::new); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/response/toss/PaymentTossDtoImpl.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.response.toss; 2 | 3 | import flab.payment_system.domain.payment.entity.toss.Cancels; 4 | import flab.payment_system.domain.payment.entity.toss.Card; 5 | import flab.payment_system.domain.payment.entity.toss.CashReceipt; 6 | import flab.payment_system.domain.payment.entity.toss.CashReceipts; 7 | import flab.payment_system.domain.payment.entity.toss.Checkout; 8 | import flab.payment_system.domain.payment.entity.toss.Discount; 9 | import flab.payment_system.domain.payment.entity.toss.EasyPay; 10 | import flab.payment_system.domain.payment.entity.toss.Failure; 11 | import flab.payment_system.domain.payment.entity.toss.GiftCertificate; 12 | import flab.payment_system.domain.payment.entity.toss.MobilePhone; 13 | import flab.payment_system.domain.payment.entity.toss.Receipt; 14 | import flab.payment_system.domain.payment.entity.toss.Transfer; 15 | import flab.payment_system.domain.payment.entity.toss.VirtualAccount; 16 | import flab.payment_system.domain.payment.response.PaymentApprovalDto; 17 | import flab.payment_system.domain.payment.response.PaymentCancelDto; 18 | import flab.payment_system.domain.payment.response.PaymentOrderDetailDto; 19 | import flab.payment_system.domain.payment.response.PaymentReadyDto; 20 | import lombok.Getter; 21 | import lombok.Setter; 22 | 23 | @Getter 24 | @Setter 25 | public class PaymentTossDtoImpl implements PaymentApprovalDto, PaymentReadyDto, 26 | PaymentOrderDetailDto, PaymentCancelDto, PaymentToss { 27 | 28 | private String version; 29 | private String paymentKey; 30 | private String type; 31 | private String orderId; 32 | private String orderName; 33 | private String mId; 34 | private String currency; 35 | private String method; 36 | private Integer totalAmount; 37 | private Integer balanceAmount; 38 | private String status; 39 | private String requestedAt; 40 | private String approvedAt; 41 | private boolean useEscrow; 42 | private String lastTransactionKey; 43 | private Integer suppliedAmount; 44 | private Integer vat; 45 | private boolean cultureExpense; 46 | private Integer taxFreeAmount; 47 | private Integer taxExemptionAmount; 48 | private Cancels[] cancels; 49 | private boolean isPartialCancelable; 50 | private Card card; 51 | private VirtualAccount virtualAccount; 52 | private String secret; 53 | private MobilePhone mobilePhone; 54 | private GiftCertificate giftCertificate; 55 | private Transfer transfer; 56 | private Receipt receipt; 57 | private EasyPay easyPay; 58 | private String country; 59 | private Failure failure; 60 | private CashReceipt cashReceipt; 61 | private CashReceipts[] cashReceipts; 62 | private Discount discount; 63 | private Checkout checkout; 64 | private Long paymentId; 65 | 66 | private String transactionKey; 67 | 68 | public void setPaymentId(Long paymentId) { 69 | this.paymentId = paymentId; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/compensation/batch/CompensationItemReader.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.compensation.batch; 2 | 3 | 4 | import flab.payment_system.domain.payment.enums.PaymentPgCompany; 5 | import flab.payment_system.domain.payment.response.toss.Settlement; 6 | import flab.payment_system.domain.payment.service.PaymentService; 7 | import flab.payment_system.domain.payment.service.PaymentStrategy; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.log4j.Log4j2; 10 | import org.springframework.batch.core.configuration.annotation.StepScope; 11 | import org.springframework.batch.core.scope.context.StepSynchronizationManager; 12 | import org.springframework.batch.item.ExecutionContext; 13 | import org.springframework.batch.item.ItemReader; 14 | import org.springframework.context.annotation.Bean; 15 | import org.springframework.context.annotation.Configuration; 16 | 17 | import java.util.Map; 18 | 19 | @Log4j2 20 | @Configuration 21 | @RequiredArgsConstructor 22 | public class CompensationItemReader { 23 | private final Map paymentStrategies; 24 | private final PaymentService paymentService; 25 | 26 | @Bean 27 | @StepScope 28 | public ItemReader paymentStrategyItemReader() { 29 | return new PaymentStrategyExecutionContextItemReader(paymentStrategies, paymentService); 30 | } 31 | 32 | public static class PaymentStrategyExecutionContextItemReader implements ItemReader { 33 | private final Map paymentStrategies; 34 | private final PaymentService paymentService; 35 | 36 | public PaymentStrategyExecutionContextItemReader(Map paymentStrategies, 37 | PaymentService paymentService) { 38 | this.paymentStrategies = paymentStrategies; 39 | this.paymentService = paymentService; 40 | } 41 | 42 | @Override 43 | public Settlement read() { 44 | ExecutionContext executionContext = StepSynchronizationManager.getContext().getStepExecution().getExecutionContext(); 45 | 46 | int currentPgCompanyIndex = executionContext.getInt("currentPgCompanyIndex", 0); 47 | int currentSettlementIndex = executionContext.getInt("currentSettlementIndex", 0); 48 | 49 | PaymentPgCompany[] pgCompanies = paymentStrategies.keySet().toArray(new PaymentPgCompany[0]); 50 | 51 | if (currentPgCompanyIndex >= pgCompanies.length) { 52 | return null; 53 | } 54 | 55 | PaymentPgCompany currentPgCompany = pgCompanies[currentPgCompanyIndex]; 56 | Settlement[] settlements = paymentService.getSettlementList(currentPgCompany); 57 | 58 | if (settlements != null && currentSettlementIndex < settlements.length) { 59 | 60 | executionContext.putInt("currentSettlementIndex", currentSettlementIndex + 1); 61 | 62 | if (currentSettlementIndex + 1 >= settlements.length) { 63 | executionContext.putInt("currentPgCompanyIndex", currentPgCompanyIndex + 1); 64 | executionContext.putInt("currentSettlementIndex", 0); // 다음 PG사의 Settlement 인덱스를 0으로 초기화 65 | } 66 | return settlements[currentSettlementIndex]; 67 | } 68 | return null; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/flab/payment_system/order/service/OrderProductServiceIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.order.service; 2 | 3 | import flab.payment_system.config.DatabaseCleanUp; 4 | import flab.payment_system.domain.order.dto.OrderDto; 5 | import flab.payment_system.domain.order.dto.OrderProductDto; 6 | import flab.payment_system.domain.order.entity.OrderProduct; 7 | import flab.payment_system.domain.order.repository.OrderRepository; 8 | import flab.payment_system.domain.order.service.OrderService; 9 | import flab.payment_system.domain.product.entity.Product; 10 | import flab.payment_system.domain.product.repository.ProductRepository; 11 | import flab.payment_system.domain.user.entity.User; 12 | import flab.payment_system.domain.user.repository.UserRepository; 13 | import org.junit.jupiter.api.*; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.boot.test.context.SpringBootTest; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertEquals; 18 | import static org.junit.jupiter.api.Assertions.assertNotNull; 19 | 20 | @SpringBootTest(properties = {"spring.config.location=classpath:application-test.yml"}) 21 | public class OrderProductServiceIntegrationTest { 22 | 23 | private final OrderService orderService; 24 | private final OrderRepository orderRepository; 25 | private final ProductRepository productRepository; 26 | private final UserRepository userRepository; 27 | private final DatabaseCleanUp databaseCleanUp; 28 | 29 | @Autowired 30 | OrderProductServiceIntegrationTest 31 | (OrderService orderService, OrderRepository orderRepository, 32 | ProductRepository productRepository, UserRepository userRepository, DatabaseCleanUp databaseCleanUp) { 33 | this.orderService = orderService; 34 | this.orderRepository = orderRepository; 35 | this.productRepository = productRepository; 36 | this.userRepository = userRepository; 37 | this.databaseCleanUp = databaseCleanUp; 38 | 39 | } 40 | 41 | @BeforeEach 42 | void setUp() { 43 | databaseCleanUp.truncateAllEntity(); 44 | Product product = new Product(); 45 | product.setName("초코파이"); 46 | product.setPrice(5000); 47 | product.setStock(100); 48 | User user = User.builder().email("test@gmail.com").password("1234").build(); 49 | userRepository.save(user); 50 | productRepository.save(product); 51 | } 52 | 53 | @AfterEach 54 | void tearDown() { 55 | databaseCleanUp.truncateAllEntity(); 56 | } 57 | 58 | 59 | @DisplayName("주문_성공") 60 | @Test 61 | public void orderProductSuccess() { 62 | // given 63 | Long userId = 1L; 64 | OrderProductDto orderProductDto = new OrderProductDto(1L, 2); 65 | // when 66 | OrderDto orderDto = orderService.orderProduct(orderProductDto, userId); 67 | OrderProduct orderProduct = orderRepository.findById(orderDto.orderId()).orElse(null); 68 | // then 69 | assertEquals(orderDto.orderId(), orderProduct.getOrderId()); 70 | assertEquals(orderProductDto.productId(), orderProduct.getProduct().getProductId()); 71 | assertEquals(userId, orderProduct.getUser().getUserId()); 72 | assertEquals(orderProductDto.quantity(), orderProduct.getQuantity()); 73 | assertNotNull(orderProduct); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.cache.CacheManager; 5 | import org.springframework.cache.annotation.EnableCaching; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.data.redis.cache.RedisCacheConfiguration; 9 | import org.springframework.data.redis.cache.RedisCacheManager; 10 | import org.springframework.data.redis.cache.RedisCacheWriter; 11 | import org.springframework.data.redis.connection.RedisConnectionFactory; 12 | import org.springframework.data.redis.connection.RedisStandaloneConfiguration; 13 | import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; 14 | import org.springframework.data.redis.core.RedisTemplate; 15 | import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; 16 | import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; 17 | import org.springframework.data.redis.serializer.RedisSerializationContext; 18 | import org.springframework.data.redis.serializer.StringRedisSerializer; 19 | import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; 20 | 21 | import java.time.Duration; 22 | 23 | @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 7200) 24 | @EnableRedisRepositories 25 | @EnableCaching 26 | @Configuration 27 | public class RedisConfig { 28 | 29 | @Value("${spring.data.redis.host}") 30 | public String host; 31 | 32 | @Value("${spring.data.redis.port}") 33 | public int port; 34 | @Value("${spring.data.redis.password}") 35 | public String password; 36 | 37 | 38 | @Bean 39 | public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { 40 | RedisTemplate redisTemplate = new RedisTemplate<>(); 41 | redisTemplate.setKeySerializer(new StringRedisSerializer()); 42 | redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); 43 | redisTemplate.setConnectionFactory(connectionFactory); 44 | return redisTemplate; 45 | } 46 | 47 | @Bean 48 | public RedisConnectionFactory redisConnectionFactory() { 49 | RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(); 50 | configuration.setHostName(host); 51 | configuration.setPort(port); 52 | configuration.setPassword(password); 53 | return new LettuceConnectionFactory(configuration); 54 | } 55 | 56 | @Bean 57 | public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { 58 | RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() 59 | .entryTtl(Duration.ofHours(12)) 60 | .disableCachingNullValues() 61 | .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) 62 | .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); 63 | 64 | return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory)) 65 | .cacheDefaults(cacheConfiguration) 66 | .build(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/compensation/batch/CompensationJobConfig.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.compensation.batch; 2 | 3 | 4 | import flab.payment_system.domain.payment.entity.Payment; 5 | import flab.payment_system.domain.payment.response.toss.Settlement; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | import lombok.extern.log4j.Log4j2; 9 | import org.springframework.batch.core.Job; 10 | import org.springframework.batch.core.Step; 11 | import org.springframework.batch.core.job.builder.JobBuilder; 12 | import org.springframework.batch.core.partition.support.Partitioner; 13 | import org.springframework.batch.core.repository.JobRepository; 14 | import org.springframework.batch.core.step.builder.StepBuilder; 15 | import org.springframework.batch.item.ExecutionContext; 16 | import org.springframework.batch.item.ItemProcessor; 17 | import org.springframework.batch.item.ItemReader; 18 | import org.springframework.batch.item.ItemWriter; 19 | import org.springframework.batch.repeat.policy.SimpleCompletionPolicy; 20 | import org.springframework.context.annotation.Bean; 21 | import org.springframework.context.annotation.Configuration; 22 | import org.springframework.core.task.TaskExecutor; 23 | import org.springframework.transaction.PlatformTransactionManager; 24 | 25 | import java.util.HashMap; 26 | import java.util.Map; 27 | 28 | 29 | @Log4j2 30 | @Configuration 31 | @RequiredArgsConstructor 32 | public class CompensationJobConfig { 33 | private static final int CHUNK = 1000; 34 | private static final int GRID_SIZE = 10; 35 | 36 | @Bean 37 | public Job paymentSyncJob(JobRepository jobRepository, Step masterStep) { 38 | return new JobBuilder("paymentSyncJob", jobRepository) 39 | .start(masterStep) 40 | .build(); 41 | } 42 | 43 | @Bean 44 | public Step masterStep( 45 | JobRepository jobRepository, 46 | Partitioner partitioner, 47 | Step slaveStep, 48 | TaskExecutor taskExecutor) { 49 | return new StepBuilder("masterStep", jobRepository) 50 | .partitioner("slaveStep", partitioner) // 파티션 설정 51 | .gridSize(GRID_SIZE) // 파티션 개수 설정 52 | .step(slaveStep) // 각 파티션을 처리할 SlaveStep 53 | .taskExecutor(taskExecutor) // 병렬 처리를 위한 TaskExecutor 설정 54 | .build(); 55 | } 56 | 57 | @Bean 58 | public Step slaveStep 59 | (PlatformTransactionManager transactionManager, 60 | JobRepository jobRepository, 61 | ItemReader paymentStrategyItemReader, 62 | ItemProcessor paymentSettlementItemProcessor, 63 | ItemWriter paymentItemWriter) { 64 | return new StepBuilder("slaveStep", jobRepository) 65 | .chunk(new SimpleCompletionPolicy(CHUNK)) 66 | .reader(paymentStrategyItemReader) 67 | .processor(paymentSettlementItemProcessor) 68 | .writer(paymentItemWriter).transactionManager(transactionManager) 69 | .build(); 70 | } 71 | 72 | @Bean 73 | public Partitioner partitioner() { 74 | return gridSize -> { 75 | Map partitions = new HashMap<>(); 76 | for (int i = 0; i < gridSize; i++) { 77 | ExecutionContext context = new ExecutionContext(); 78 | context.putInt("partitionNumber", i); // 각 파티션에 고유 번호 부여 79 | partitions.put("partition" + i, context); 80 | } 81 | return partitions; 82 | }; 83 | } 84 | } 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/controller/PaymentController.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.controller; 2 | 3 | import org.springframework.http.ResponseEntity; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.PathVariable; 6 | import org.springframework.web.bind.annotation.PostMapping; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RequestParam; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import flab.payment_system.adapter.PaymentAdapter; 13 | import flab.payment_system.common.response.ResponseMessage; 14 | import flab.payment_system.domain.payment.dto.PaymentCreateDto; 15 | import flab.payment_system.domain.payment.enums.PaymentPgCompany; 16 | import flab.payment_system.domain.payment.response.PaymentApprovalDto; 17 | import flab.payment_system.domain.payment.response.PaymentOrderDetailDto; 18 | import flab.payment_system.domain.payment.response.PaymentReadyDto; 19 | import flab.payment_system.domain.payment.service.PaymentService; 20 | import jakarta.servlet.http.HttpServletRequest; 21 | import jakarta.servlet.http.HttpSession; 22 | import jakarta.validation.Valid; 23 | import lombok.RequiredArgsConstructor; 24 | 25 | @RestController 26 | @RequiredArgsConstructor 27 | @RequestMapping("/api/v1/payment") 28 | public class PaymentController { 29 | 30 | private final PaymentService paymentService; 31 | private final PaymentAdapter paymentAdapter; 32 | 33 | @PostMapping("/{pgCompany}") 34 | public ResponseEntity createPayment( 35 | @PathVariable PaymentPgCompany pgCompany, 36 | @RequestBody @Valid PaymentCreateDto paymentCreateDto, 37 | HttpServletRequest request, HttpSession session) { 38 | String requestUrl = request.getRequestURL().toString() 39 | .replace(request.getRequestURI(), ""); 40 | 41 | Long userId = paymentAdapter.getUserId(session); 42 | 43 | PaymentReadyDto paymentReadyDto = paymentService.createPayment(paymentCreateDto, requestUrl, 44 | userId, pgCompany); 45 | 46 | return ResponseEntity.ok().body(paymentReadyDto); 47 | } 48 | 49 | @GetMapping("/{pgCompany}/approved") 50 | public ResponseEntity paymentApproved( 51 | @PathVariable PaymentPgCompany pgCompany, 52 | @RequestParam("pg_token") String pgToken, @RequestParam("orderId") Long orderId, 53 | @RequestParam("paymentId") Long paymentId, @RequestParam("productId") Long productId 54 | , @RequestParam("quantity") Integer quantity, 55 | HttpSession session) { 56 | Long userId = paymentAdapter.getUserId(session); 57 | PaymentApprovalDto paymentApprovalDto = paymentService.approvePayment(pgToken, orderId, userId, paymentId, 58 | productId, quantity, pgCompany); 59 | 60 | return ResponseEntity.ok().body(paymentApprovalDto); 61 | } 62 | 63 | @GetMapping("/{pgCompany}/fail") 64 | public ResponseEntity paymentFail(@PathVariable PaymentPgCompany pgCompany, 65 | @RequestParam("paymentId") Long paymentId) { 66 | paymentService.failPayment(paymentId); 67 | return ResponseEntity.ok().body(new ResponseMessage("fail")); 68 | } 69 | 70 | @GetMapping("/{pgCompany}") 71 | public ResponseEntity paymentOrderDetail( 72 | @PathVariable PaymentPgCompany pgCompany, @RequestParam String paymentKey) { 73 | PaymentOrderDetailDto paymentOrderDetailDto = paymentService.getOrderDetail(paymentKey, pgCompany); 74 | 75 | return ResponseEntity.ok().body(paymentOrderDetailDto); 76 | } 77 | } 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/config/AppConfig.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.config; 2 | 3 | 4 | import flab.payment_system.common.enums.Constant; 5 | import flab.payment_system.common.filter.ExceptionHandlerFilter; 6 | import flab.payment_system.common.filter.SignInCheckFilter; 7 | import flab.payment_system.domain.session.service.SessionService; 8 | import lombok.RequiredArgsConstructor; 9 | import org.apache.hc.client5.http.classic.HttpClient; 10 | import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; 11 | import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; 12 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.context.annotation.Configuration; 15 | import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; 16 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 17 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 18 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 19 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 20 | import org.springframework.security.crypto.password.PasswordEncoder; 21 | import org.springframework.security.web.SecurityFilterChain; 22 | import org.springframework.web.client.RestTemplate; 23 | import org.springframework.web.filter.OncePerRequestFilter; 24 | 25 | @Configuration 26 | @EnableWebSecurity 27 | @RequiredArgsConstructor 28 | public class AppConfig { 29 | 30 | private final SessionService sessionService; 31 | 32 | @Bean 33 | public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { 34 | return http 35 | .csrf(AbstractHttpConfigurer::disable) 36 | .authorizeHttpRequests((authorizeRequests) -> { 37 | authorizeRequests.anyRequest().permitAll(); 38 | }) 39 | 40 | .formLogin(AbstractHttpConfigurer::disable) 41 | 42 | .build(); 43 | } 44 | 45 | @Bean 46 | public FilterRegistrationBean SignInCheckFilter() { 47 | 48 | FilterRegistrationBean bean = new FilterRegistrationBean<>(); 49 | 50 | bean.setFilter( 51 | new SignInCheckFilter(sessionService)); 52 | bean.setOrder(2); 53 | bean.addUrlPatterns( 54 | Constant.API_AND_VERSION.getValue() + "/order/*", 55 | Constant.API_AND_VERSION.getValue() + "/payments/*"); 56 | 57 | return bean; 58 | } 59 | 60 | @Bean 61 | public FilterRegistrationBean ExceptionHandlerFilter() { 62 | 63 | FilterRegistrationBean bean = new FilterRegistrationBean<>(); 64 | 65 | bean.setFilter( 66 | new ExceptionHandlerFilter()); 67 | bean.setOrder(1); 68 | bean.addUrlPatterns( 69 | Constant.API_AND_VERSION.getValue() + "/order/*", 70 | Constant.API_AND_VERSION.getValue() + "/payments/*"); 71 | 72 | return bean; 73 | } 74 | 75 | @Bean 76 | public PasswordEncoder passwordEncoder() { 77 | return new BCryptPasswordEncoder(10); 78 | } 79 | 80 | @Bean 81 | HttpClient httpClient() { 82 | return HttpClientBuilder.create() 83 | .setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create() 84 | .setMaxConnPerRoute(30) 85 | .setMaxConnTotal(60) 86 | .build()) 87 | .build(); 88 | } 89 | 90 | @Bean 91 | HttpComponentsClientHttpRequestFactory factory(HttpClient httpClient) { 92 | HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); 93 | factory.setConnectTimeout(3000); 94 | factory.setHttpClient(httpClient); 95 | 96 | return factory; 97 | } 98 | 99 | @Bean 100 | RestTemplate restTemplate(HttpComponentsClientHttpRequestFactory factory) { 101 | return new RestTemplate(factory); 102 | } 103 | 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/request/toss/PaymentTossRequestBodyFactory.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.request.toss; 2 | 3 | import flab.payment_system.common.enums.Constant; 4 | import flab.payment_system.domain.order.dto.OrderCancelDto; 5 | import flab.payment_system.domain.payment.dto.PaymentCreateDto; 6 | import flab.payment_system.domain.payment.entity.Payment; 7 | import flab.payment_system.domain.payment.enums.PaymentPgCompany; 8 | import flab.payment_system.domain.payment.exception.PaymentNotExistBadRequestException; 9 | import flab.payment_system.domain.payment.repository.PaymentRepository; 10 | 11 | import java.util.Base64; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | import org.springframework.beans.factory.annotation.Value; 16 | import org.springframework.http.HttpEntity; 17 | import org.springframework.http.HttpHeaders; 18 | import org.springframework.http.MediaType; 19 | import org.springframework.stereotype.Component; 20 | 21 | @Component 22 | public class PaymentTossRequestBodyFactory { 23 | 24 | 25 | private final String secretKey; 26 | private final PaymentRepository paymentRepository; 27 | 28 | public PaymentTossRequestBodyFactory(@Value("${toss-secret-key}") String secretKey 29 | , PaymentRepository paymentRepository) { 30 | this.secretKey = secretKey; 31 | this.paymentRepository = paymentRepository; 32 | } 33 | 34 | 35 | public HttpEntity> getBodyForCreatePayment( 36 | PaymentCreateDto paymentCreateDto, Long userId, String requestUrl, Long paymentId) { 37 | HttpHeaders headers = getHeaders(); 38 | Map params = new HashMap<>(); 39 | 40 | params.put("method", "간편결제"); 41 | params.put("taxFreeAmount", String.valueOf(paymentCreateDto.taxFreeAmount())); 42 | params.put("orderId", "orderId_" + paymentCreateDto.orderId() + "_" + userId); 43 | params.put("orderName", paymentCreateDto.productName() + " " + paymentCreateDto.quantity()); 44 | params.put("amount", 45 | String.valueOf(paymentCreateDto.totalAmount())); 46 | params.put("successUrl", 47 | requestUrl + Constant.API_AND_VERSION.getValue() + "/payment/" 48 | + PaymentPgCompany.TOSS.getName() + 49 | "/approved?orderId=" + paymentCreateDto.orderId() + "&paymentId=" + paymentId + "&pg_token=temp" 50 | + "&productId=" 51 | + paymentCreateDto.productId() + "&quantity=" + paymentCreateDto.quantity()); 52 | params.put("failUrl", 53 | requestUrl + Constant.API_AND_VERSION.getValue() + "/payment/" 54 | + PaymentPgCompany.TOSS.getName() + "/cancel?paymentId=" + paymentId); 55 | 56 | return new HttpEntity<>(params, headers); 57 | } 58 | 59 | public HttpEntity> getBodyForApprovePayment( 60 | Long orderId, Long userId, Long paymentId) { 61 | HttpHeaders headers = getHeaders(); 62 | 63 | Map params = new HashMap<>(); 64 | 65 | Payment payment = paymentRepository.findById(paymentId) 66 | .orElseThrow(PaymentNotExistBadRequestException::new); 67 | 68 | params.put("paymentKey", payment.getPaymentKey()); 69 | params.put("orderId", "orderId_" + orderId + "_" + userId); 70 | params.put("amount", String.valueOf(payment.getTotalAmount())); 71 | 72 | return new HttpEntity<>(params, headers); 73 | } 74 | 75 | 76 | public HttpEntity> getBodyForCancelPayment( 77 | OrderCancelDto orderCancelDto) { 78 | HttpHeaders headers = getHeaders(); 79 | Map params = new HashMap<>(); 80 | 81 | params.put("cancelReason", "전액취소"); 82 | params.put("cancelAmount", String.valueOf(orderCancelDto.cancelAmount())); 83 | return new HttpEntity<>(params, headers); 84 | } 85 | 86 | public HttpHeaders getHeaders() { 87 | HttpHeaders headers = new HttpHeaders(); 88 | String authorization = Base64.getEncoder().encodeToString((secretKey + ":").getBytes()); 89 | headers.set("Authorization", "Basic " + authorization); 90 | headers.setContentType(MediaType.APPLICATION_JSON); 91 | return headers; 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/test/java/flab/payment_system/user/service/UserServiceIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.user.service; 2 | 3 | 4 | import flab.payment_system.config.DatabaseCleanUp; 5 | import flab.payment_system.domain.user.entity.User; 6 | import flab.payment_system.domain.user.entity.UserVerification; 7 | import flab.payment_system.domain.user.dto.UserConfirmVerificationNumberDto; 8 | import flab.payment_system.domain.user.dto.UserSignUpDto; 9 | import flab.payment_system.domain.user.dto.UserVerificationDto; 10 | import flab.payment_system.domain.user.dto.UserVerifyEmailDto; 11 | import flab.payment_system.domain.user.repository.UserRepository; 12 | import flab.payment_system.domain.user.repository.UserVerificationRepository; 13 | import flab.payment_system.domain.user.service.UserService; 14 | import org.junit.jupiter.api.*; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.test.context.SpringBootTest; 17 | 18 | import static org.junit.jupiter.api.Assertions.*; 19 | 20 | @SpringBootTest(properties = {"spring.config.location=classpath:application-test.yml"}) 21 | public class UserServiceIntegrationTest { 22 | 23 | private final UserService userService; 24 | 25 | private final UserRepository userRepository; 26 | 27 | private final UserVerificationRepository userVerificationRepository; 28 | 29 | private final DatabaseCleanUp databaseCleanUp; 30 | 31 | @Autowired 32 | UserServiceIntegrationTest 33 | (UserService userService, UserRepository userRepository, 34 | UserVerificationRepository userVerificationRepository, DatabaseCleanUp databaseCleanUp) { 35 | this.userService = userService; 36 | this.userRepository = userRepository; 37 | this.userVerificationRepository = userVerificationRepository; 38 | this.databaseCleanUp = databaseCleanUp; 39 | 40 | } 41 | 42 | @BeforeEach 43 | void setUp() { 44 | databaseCleanUp.truncateAllEntity(); 45 | } 46 | 47 | @AfterEach 48 | void tearDown() { 49 | databaseCleanUp.truncateAllEntity(); 50 | } 51 | 52 | 53 | @DisplayName("이메일_인증번호_발급_성공") 54 | @Test 55 | public void verifyUserEmailSuccess() { 56 | // given 57 | String email = "payment@test.com"; 58 | UserVerifyEmailDto userVerifyEmailDto = new UserVerifyEmailDto(email); 59 | 60 | // when 61 | UserVerificationDto userVerificationDto = userService.verifyUserEmail(userVerifyEmailDto); 62 | 63 | // then 64 | assertEquals(userVerificationDto.email(), email); 65 | 66 | } 67 | 68 | @DisplayName("이메일_인증_성공") 69 | @Test 70 | public void confirmVerificationNumberSuccess() { 71 | // given 72 | String email = "payment@test.com"; 73 | UserVerifyEmailDto userVerifyEmailDto = new UserVerifyEmailDto(email); 74 | UserVerificationDto userVerificationDto = userService.verifyUserEmail(userVerifyEmailDto); 75 | 76 | UserConfirmVerificationNumberDto userConfirmVerificationNumberDto = new UserConfirmVerificationNumberDto( 77 | userVerificationDto.verificationId(), userVerificationDto.email(), 78 | userVerificationDto.verificationNumber()); 79 | 80 | // when 81 | boolean isVerified = userService.confirmVerificationNumber( 82 | userConfirmVerificationNumberDto); 83 | 84 | // then 85 | assertTrue(isVerified); 86 | } 87 | 88 | @DisplayName("회원가입_성공") 89 | @Test 90 | public void signUpSuccess() { 91 | // given 92 | String email = "payment@test.com"; 93 | String password = "12345"; 94 | 95 | UserVerification userVerification = userVerificationRepository.save( 96 | UserVerification.builder() 97 | .email(email) 98 | .verificationNumber(123456).isVerified(false).build()); 99 | 100 | UserSignUpDto userSignUpDto = new UserSignUpDto(userVerification.getVerificationId(), 101 | userVerification.getEmail(), password, password); 102 | 103 | userService.confirmVerificationNumber(new UserConfirmVerificationNumberDto( 104 | userVerification.getVerificationId(), userVerification.getEmail(), 105 | userVerification.getVerificationNumber())); 106 | 107 | // when 108 | userService.signUpUser(userSignUpDto); 109 | User user = userRepository.findByEmail(email).orElse(null); 110 | 111 | // then 112 | assertEquals(email, user.getEmail()); 113 | assertNotNull(user); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/common/exception/CustomExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.common.exception; 2 | 3 | 4 | import jakarta.mail.MessagingException; 5 | import java.io.UnsupportedEncodingException; 6 | import java.util.List; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.log4j.Log4j2; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.HttpStatusCode; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.http.converter.HttpMessageNotReadableException; 13 | import org.springframework.web.bind.MethodArgumentNotValidException; 14 | import org.springframework.web.bind.annotation.ExceptionHandler; 15 | import org.springframework.web.bind.annotation.RestControllerAdvice; 16 | 17 | @Log4j2 18 | @RequiredArgsConstructor 19 | @RestControllerAdvice 20 | public class CustomExceptionHandler { 21 | 22 | @ExceptionHandler(BaseException.class) 23 | public ResponseEntity handleBaseException(BaseException baseException) { 24 | ExceptionMessage exceptionMessage = ExceptionMessage.builder() 25 | .message(baseException.getStatus() + " : " + baseException.getMessage()) 26 | .code(baseException.getCode()).build(); 27 | 28 | log.info(baseException.getStatus() + " : " + baseException.getMessage(), baseException); 29 | return new ResponseEntity<> 30 | (exceptionMessage, HttpStatusCode.valueOf(baseException.getCode())); 31 | } 32 | 33 | @ExceptionHandler(MessagingException.class) 34 | public ResponseEntity handleMessagingException(Exception exception) { 35 | ExceptionMessage exceptionMessage = ExceptionMessage.builder() 36 | .message(HttpStatus.INTERNAL_SERVER_ERROR + " : messaging_exception") 37 | .code(HttpStatus.INTERNAL_SERVER_ERROR.value()).build(); 38 | 39 | log.warn(HttpStatus.INTERNAL_SERVER_ERROR + " : messaging_exception", exception); 40 | return new ResponseEntity<> 41 | (exceptionMessage, HttpStatus.INTERNAL_SERVER_ERROR); 42 | } 43 | 44 | @ExceptionHandler(UnsupportedEncodingException.class) 45 | public ResponseEntity handleUnsupportedEncodingException( 46 | Exception exception) { 47 | ExceptionMessage exceptionMessage = ExceptionMessage.builder() 48 | .message(HttpStatus.UNSUPPORTED_MEDIA_TYPE + " : unsupportedEncodingException") 49 | .code(HttpStatus.UNSUPPORTED_MEDIA_TYPE.value()).build(); 50 | 51 | log.warn(HttpStatus.UNSUPPORTED_MEDIA_TYPE + " : unsupportedEncodingException", exception); 52 | return new ResponseEntity<> 53 | (exceptionMessage, HttpStatus.UNSUPPORTED_MEDIA_TYPE); 54 | } 55 | 56 | @ExceptionHandler(MethodArgumentNotValidException.class) 57 | public ResponseEntity handleMethodArgumentNotValidException( 58 | MethodArgumentNotValidException exception) { 59 | List errors = exception.getBindingResult() 60 | .getFieldErrors() 61 | .stream() 62 | .map(x -> x.getDefaultMessage()).toList(); 63 | 64 | ExceptionMessage exceptionMessage = ExceptionMessage.builder() 65 | .message(HttpStatus.BAD_REQUEST + " : " + errors) 66 | .code(HttpStatus.BAD_REQUEST.value()).build(); 67 | 68 | log.info(HttpStatus.BAD_REQUEST + " : " + errors, exception); 69 | return new ResponseEntity<> 70 | (exceptionMessage, HttpStatus.BAD_REQUEST); 71 | } 72 | 73 | @ExceptionHandler(HttpMessageNotReadableException.class) 74 | public ResponseEntity handleHttpMessageNotReadableException( 75 | Exception exception) { 76 | 77 | ExceptionMessage exceptionMessage = ExceptionMessage.builder() 78 | .message(HttpStatus.BAD_REQUEST + " : " + exception.getMessage()) 79 | .code(HttpStatus.BAD_REQUEST.value()).build(); 80 | 81 | log.warn(HttpStatus.BAD_REQUEST + " : " + exception.getMessage(), exception); 82 | return new ResponseEntity<> 83 | (exceptionMessage, HttpStatus.BAD_REQUEST); 84 | } 85 | 86 | @ExceptionHandler(RuntimeException.class) 87 | public ResponseEntity handleRuntimeException( 88 | Exception exception) { 89 | 90 | ExceptionMessage exceptionMessage = ExceptionMessage.builder() 91 | .message(HttpStatus.INTERNAL_SERVER_ERROR + " : " + exception.getMessage()) 92 | .code(HttpStatus.INTERNAL_SERVER_ERROR.value()).build(); 93 | 94 | log.error(HttpStatus.INTERNAL_SERVER_ERROR + " : " + exception.getMessage(), exception); 95 | return new ResponseEntity<> 96 | (exceptionMessage, HttpStatus.INTERNAL_SERVER_ERROR); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/test/java/flab/payment_system/product/service/ProductServiceIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.product.service; 2 | 3 | import flab.payment_system.config.DatabaseCleanUp; 4 | import flab.payment_system.domain.product.dto.ProductDto; 5 | import flab.payment_system.domain.product.entity.Product; 6 | import flab.payment_system.domain.product.exception.ProductNotExistBadRequestException; 7 | import flab.payment_system.domain.product.repository.ProductRepository; 8 | import flab.payment_system.domain.product.service.ProductService; 9 | import org.junit.jupiter.api.*; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.context.SpringBootTest; 12 | 13 | import java.util.List; 14 | import java.util.stream.IntStream; 15 | 16 | import static org.junit.jupiter.api.Assertions.*; 17 | 18 | @SpringBootTest(properties = {"spring.config.location=classpath:application-test.yml"}) 19 | public class ProductServiceIntegrationTest { 20 | 21 | private final ProductService productService; 22 | private final ProductRepository productRepository; 23 | 24 | private final DatabaseCleanUp databaseCleanUp; 25 | private Product product; 26 | 27 | @Autowired 28 | ProductServiceIntegrationTest 29 | (ProductService productService, ProductRepository productRepository, 30 | DatabaseCleanUp databaseCleanUp) { 31 | this.productService = productService; 32 | this.productRepository = productRepository; 33 | this.databaseCleanUp = databaseCleanUp; 34 | 35 | } 36 | 37 | @BeforeEach 38 | void setUp() { 39 | databaseCleanUp.truncateAllEntity(); 40 | product = new Product(); 41 | product.setName("초코파이"); 42 | product.setPrice(1000); 43 | product.setStock(100); 44 | productRepository.save(product); 45 | } 46 | 47 | @AfterEach 48 | void tearDown() { 49 | databaseCleanUp.truncateAllEntity(); 50 | } 51 | 52 | 53 | @DisplayName("최초 상품목록_조회") 54 | @Test 55 | public void getRecentProductListSuccess() { 56 | // given 57 | IntStream.rangeClosed(1, 10).forEach(i -> { 58 | Product product = new Product(); 59 | product.setName("Product" + i); 60 | product.setPrice(i * 1000); 61 | product.setStock(i * 10); 62 | productRepository.save(product); 63 | }); 64 | 65 | Long lastProductId = null; 66 | long size = 5; 67 | 68 | 69 | // when 70 | List productList = productService.getProductList(lastProductId, size); 71 | 72 | // then 73 | assertEquals(size, productList.size(), "조회된 상품 목록의 크기가 요청 크기와 일치해야 합니다."); 74 | assertTrue( 75 | IntStream.range(0, productList.size() - 1) 76 | .allMatch(i -> productList.get(i).productId() > productList.get(i + 1).productId()), 77 | "상품 목록은 내림차순으로 정렬되어야 합니다." 78 | ); 79 | } 80 | 81 | @DisplayName("상품목록_조회") 82 | @Test 83 | public void getNextProductList() { 84 | // given 85 | IntStream.rangeClosed(1, 10).forEach(i -> { 86 | Product product = new Product(); 87 | product.setName("Product" + i); 88 | product.setPrice(i * 1000); 89 | product.setStock(i * 10); 90 | productRepository.save(product); 91 | }); 92 | 93 | Long lastProductId = 6L; 94 | long size = 5; 95 | 96 | 97 | // when 98 | List productList = productService.getProductList(lastProductId, size); 99 | 100 | 101 | // then 102 | assertEquals(size, productList.size(), "조회된 상품 목록의 크기가 요청 크기와 일치해야 합니다."); 103 | assertTrue( 104 | IntStream.range(5, productList.size() - 1) 105 | .allMatch(i -> productList.get(i).productId() > productList.get(i + 1).productId()), 106 | "상품 목록은 내림차순으로 정렬되어야 합니다." 107 | ); 108 | } 109 | 110 | @Test 111 | @DisplayName("상품상세조회_성공") 112 | void getProductDetailSuccess() { 113 | // given 114 | Long productId = 1L; 115 | 116 | // when 117 | ProductDto result = productService.getProductDetail(productId); 118 | 119 | // then 120 | assertEquals(result.productId(), product.getProductId()); 121 | assertEquals(result.name(), product.getName()); 122 | assertEquals(result.price(), product.getPrice()); 123 | assertEquals(result.stock(), product.getStock()); 124 | } 125 | 126 | @Test 127 | @DisplayName("상품상세조회_실패") 128 | void getProductDetailFailure() { 129 | // given 130 | Long invalidProductId = 2L; 131 | 132 | // when & then 133 | assertThrows(ProductNotExistBadRequestException.class, () -> { 134 | productService.getProductDetail(invalidProductId); 135 | }); 136 | } 137 | 138 | 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/service/kakao/PaymentStrategyKaKaoService.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.service.kakao; 2 | 3 | import flab.payment_system.domain.order.dto.OrderCancelDto; 4 | import flab.payment_system.domain.payment.dto.PaymentCreateDto; 5 | import flab.payment_system.domain.payment.client.kakao.PaymentKakaoClient; 6 | import flab.payment_system.domain.payment.entity.kakao.KakaoPayment; 7 | import flab.payment_system.domain.payment.enums.PaymentKakaoEndpoint; 8 | import flab.payment_system.domain.payment.repository.kakao.KakaoPaymentRepository; 9 | import flab.payment_system.domain.payment.request.kakao.PaymentKakaoRequestBodyFactory; 10 | import flab.payment_system.domain.payment.response.PaymentApprovalDto; 11 | import flab.payment_system.domain.payment.response.PaymentCancelDto; 12 | import flab.payment_system.domain.payment.response.PaymentOrderDetailDto; 13 | import flab.payment_system.domain.payment.response.PaymentReadyDto; 14 | import flab.payment_system.domain.payment.response.kakao.PaymentKakaoApprovalDtoImpl; 15 | import flab.payment_system.domain.payment.response.toss.Settlement; 16 | import flab.payment_system.domain.payment.service.PaymentService; 17 | import flab.payment_system.domain.payment.service.PaymentStrategy; 18 | 19 | import org.springframework.beans.factory.annotation.Value; 20 | import org.springframework.http.HttpEntity; 21 | import org.springframework.stereotype.Component; 22 | import org.springframework.util.MultiValueMap; 23 | 24 | @Component 25 | public class PaymentStrategyKaKaoService implements PaymentStrategy { 26 | 27 | private final String kakaoHost; 28 | private final KakaoPaymentRepository kakaoPaymentRepository; 29 | private final PaymentKakaoRequestBodyFactory paymentKakaoRequestBodyFactory; 30 | private final PaymentKakaoClient paymentKakaoClient; 31 | private final PaymentService paymentService; 32 | 33 | public PaymentStrategyKaKaoService( 34 | @Value("${kakao-host}") String kakaoHost, 35 | KakaoPaymentRepository kakaoPaymentRepository, 36 | PaymentKakaoRequestBodyFactory paymentKakaoRequestBodyFactory, 37 | PaymentKakaoClient paymentKakaoClient, PaymentService paymentService) { 38 | this.kakaoHost = kakaoHost; 39 | this.kakaoPaymentRepository = kakaoPaymentRepository; 40 | this.paymentKakaoRequestBodyFactory = paymentKakaoRequestBodyFactory; 41 | this.paymentKakaoClient = paymentKakaoClient; 42 | this.paymentService = paymentService; 43 | } 44 | 45 | @Override 46 | public PaymentReadyDto createPayment(PaymentCreateDto paymentCreateDto, Long userId, String requestUrl, 47 | Long paymentId) { 48 | 49 | HttpEntity> body = paymentKakaoRequestBodyFactory.getBodyForCreatePayment( 50 | paymentCreateDto, userId, requestUrl, paymentId); 51 | 52 | return paymentKakaoClient.createPayment(kakaoHost + PaymentKakaoEndpoint.READY.getEndpoint(), body); 53 | } 54 | 55 | @Override 56 | public PaymentApprovalDto approvePayment(String pgToken, Long orderId, Long userId, 57 | Long paymentId) { 58 | HttpEntity> body = paymentKakaoRequestBodyFactory.getBodyForApprovePayment( 59 | pgToken, orderId, 60 | userId, paymentId); 61 | 62 | PaymentKakaoApprovalDtoImpl paymentApprovalDto = paymentKakaoClient.approvePayment( 63 | kakaoHost + PaymentKakaoEndpoint.APPROVE.getEndpoint(), body); 64 | 65 | kakaoPaymentRepository.save(KakaoPayment.builder().payment(paymentService.getPaymentByPaymentId(paymentId)) 66 | .paymentMethodType(paymentApprovalDto.getPaymentMethodType()).aid( 67 | paymentApprovalDto.getAid()) 68 | .cardInfo(paymentApprovalDto.getCardInfo()) 69 | .paymentMethodType(paymentApprovalDto.getPaymentMethodType()).build()); 70 | 71 | return paymentApprovalDto; 72 | } 73 | 74 | @Override 75 | public PaymentCancelDto cancelPayment(OrderCancelDto orderCancelDto) { 76 | HttpEntity> body = paymentKakaoRequestBodyFactory.getBodyForCancelPayment( 77 | orderCancelDto); 78 | 79 | return paymentKakaoClient.cancelPayment(kakaoHost + PaymentKakaoEndpoint.CANCEL.getEndpoint(), 80 | body); 81 | } 82 | 83 | @Override 84 | public PaymentOrderDetailDto getOrderDetail(String tid) { 85 | HttpEntity> body = paymentKakaoRequestBodyFactory.getBodyForOrderDetail( 86 | tid); 87 | 88 | return paymentKakaoClient.getOrderDetail(kakaoHost + PaymentKakaoEndpoint.ORDER.getEndpoint(), body); 89 | } 90 | 91 | @Override 92 | public Settlement[] getSettlementList() { 93 | // 카카오에서는 실제 현금이 오가야 정산 API 제공해서 연동x 94 | return null; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/service/toss/PaymentStrategyTossService.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.service.toss; 2 | 3 | import flab.payment_system.common.util.DateUtil; 4 | import flab.payment_system.domain.order.dto.OrderCancelDto; 5 | import flab.payment_system.domain.payment.dto.PaymentCreateDto; 6 | import flab.payment_system.domain.payment.client.toss.PaymentTossClient; 7 | import flab.payment_system.domain.payment.entity.toss.TossPayment; 8 | import flab.payment_system.domain.payment.enums.PaymentTossEndpoint; 9 | import flab.payment_system.domain.payment.repository.toss.TossPaymentRepository; 10 | import flab.payment_system.domain.payment.request.toss.PaymentTossRequestBodyFactory; 11 | import flab.payment_system.domain.payment.response.PaymentApprovalDto; 12 | import flab.payment_system.domain.payment.response.PaymentCancelDto; 13 | import flab.payment_system.domain.payment.response.PaymentOrderDetailDto; 14 | import flab.payment_system.domain.payment.response.PaymentReadyDto; 15 | import flab.payment_system.domain.payment.response.toss.PaymentTossDtoImpl; 16 | import flab.payment_system.domain.payment.response.toss.Settlement; 17 | import flab.payment_system.domain.payment.service.PaymentService; 18 | import flab.payment_system.domain.payment.service.PaymentStrategy; 19 | 20 | import java.util.Map; 21 | 22 | import org.springframework.beans.factory.annotation.Value; 23 | import org.springframework.http.HttpEntity; 24 | import org.springframework.http.HttpHeaders; 25 | import org.springframework.stereotype.Component; 26 | 27 | @Component 28 | public class PaymentStrategyTossService implements PaymentStrategy { 29 | 30 | private final String tossHost; 31 | private final TossPaymentRepository tossPaymentRepository; 32 | private final PaymentTossRequestBodyFactory paymentTossRequestBodyFactory; 33 | private final PaymentTossClient paymentTossClient; 34 | private final PaymentService paymentService; 35 | 36 | public PaymentStrategyTossService( 37 | @Value("${toss-host}") String tossHost, 38 | TossPaymentRepository tossPaymentRepository, 39 | PaymentTossRequestBodyFactory paymentTossRequestBodyFactory, 40 | PaymentTossClient paymentTossClient, PaymentService paymentService) { 41 | this.tossHost = tossHost; 42 | this.tossPaymentRepository = tossPaymentRepository; 43 | this.paymentTossRequestBodyFactory = paymentTossRequestBodyFactory; 44 | this.paymentTossClient = paymentTossClient; 45 | this.paymentService = paymentService; 46 | } 47 | 48 | @Override 49 | public PaymentReadyDto createPayment(PaymentCreateDto paymentCreateDto, Long userId, 50 | String requestUrl, Long paymentId) { 51 | HttpEntity> body = paymentTossRequestBodyFactory.getBodyForCreatePayment( 52 | paymentCreateDto, userId, requestUrl, paymentId); 53 | return paymentTossClient.createPayment(tossHost, body); 54 | } 55 | 56 | @Override 57 | public PaymentApprovalDto approvePayment(String pgToken, Long orderId, Long userId, 58 | Long paymentId) { 59 | HttpEntity> body = paymentTossRequestBodyFactory.getBodyForApprovePayment( 60 | orderId, userId, paymentId); 61 | PaymentTossDtoImpl paymentTossDto = paymentTossClient.approvePayment(tossHost + PaymentTossEndpoint.PAYMENT.getEndpoint() 62 | + PaymentTossEndpoint.APPROVE.getEndpoint(), body); 63 | 64 | tossPaymentRepository.save( 65 | TossPayment.builder() 66 | .payment(paymentService.getPaymentByPaymentId(paymentId)) 67 | .country(paymentTossDto.getCountry()) 68 | .currency( 69 | paymentTossDto.getCurrency()) 70 | .type(paymentTossDto.getType()) 71 | .build()); 72 | 73 | return paymentTossDto; 74 | } 75 | 76 | @Override 77 | public PaymentCancelDto cancelPayment(OrderCancelDto orderCancelDto) { 78 | 79 | HttpEntity> body = paymentTossRequestBodyFactory.getBodyForCancelPayment( 80 | orderCancelDto); 81 | return paymentTossClient.cancelPayment( 82 | tossHost + PaymentTossEndpoint.PAYMENT.getEndpoint() + "/" + orderCancelDto.paymentKey() + PaymentTossEndpoint.CANCEL, body); 83 | } 84 | 85 | @Override 86 | public PaymentOrderDetailDto getOrderDetail(String paymentKey) { 87 | HttpHeaders headers = paymentTossRequestBodyFactory.getHeaders(); 88 | HttpEntity requestEntity = new HttpEntity<>(headers); 89 | return paymentTossClient.getOrderDetail(tossHost + PaymentTossEndpoint.PAYMENT.getEndpoint() + "/" + paymentKey, requestEntity); 90 | } 91 | 92 | @Override 93 | public Settlement[] getSettlementList() { 94 | String yesterdayDate = DateUtil.getYesterdayDate(); 95 | 96 | HttpHeaders headers = paymentTossRequestBodyFactory.getHeaders(); 97 | HttpEntity requestEntity = new HttpEntity<>(headers); 98 | String url = tossHost + PaymentTossEndpoint.SETTLEMENT.getEndpoint() 99 | + "?startDate=" + yesterdayDate + "&endDate=" + yesterdayDate; 100 | return paymentTossClient.getSettlementList(url, requestEntity); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/request/kakao/PaymentKakaoRequestBodyFactory.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.request.kakao; 2 | 3 | import flab.payment_system.common.enums.Constant; 4 | import flab.payment_system.domain.order.dto.OrderCancelDto; 5 | import flab.payment_system.domain.payment.dto.PaymentCreateDto; 6 | import flab.payment_system.domain.payment.entity.Payment; 7 | import flab.payment_system.domain.payment.enums.PaymentPgCompany; 8 | import flab.payment_system.domain.payment.exception.PaymentNotExistBadRequestException; 9 | import flab.payment_system.domain.payment.repository.PaymentRepository; 10 | 11 | import java.util.Optional; 12 | 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.http.HttpEntity; 15 | import org.springframework.http.HttpHeaders; 16 | import org.springframework.http.MediaType; 17 | import org.springframework.stereotype.Component; 18 | import org.springframework.util.LinkedMultiValueMap; 19 | import org.springframework.util.MultiValueMap; 20 | 21 | @Component 22 | public class PaymentKakaoRequestBodyFactory { 23 | 24 | private final String cid; 25 | private final PaymentRepository paymentRepository; 26 | private final String adminKey; 27 | 28 | public PaymentKakaoRequestBodyFactory(@Value("${kakao-cid}") String cid, 29 | @Value("${kakao-adminkey}") String adminKey, 30 | PaymentRepository paymentRepository) { 31 | this.cid = cid; 32 | this.adminKey = adminKey; 33 | this.paymentRepository = paymentRepository; 34 | } 35 | 36 | 37 | private HttpHeaders getHeaders() { 38 | HttpHeaders headers = new HttpHeaders(); 39 | headers.add("Authorization", "KakaoAK " + adminKey); 40 | headers.add("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"); 41 | return headers; 42 | } 43 | 44 | public HttpEntity> getBodyForCreatePayment( 45 | PaymentCreateDto paymentCreateDto, Long userId, String requestUrl, Long paymentId) { 46 | HttpHeaders headers = getHeaders(); 47 | 48 | MultiValueMap params = new LinkedMultiValueMap<>(); 49 | Optional installMonth = paymentCreateDto.getInstallMonth(); 50 | installMonth.ifPresent(integer -> params.add("install_month", String.valueOf(integer))); 51 | params.add("cid", cid); 52 | params.add("approval_url", 53 | requestUrl + Constant.API_AND_VERSION.getValue() + "/payment/" 54 | + PaymentPgCompany.KAKAO.getName() + 55 | "/approved?orderId=" + paymentCreateDto.orderId() + "&paymentId=" + paymentId + "&productId=" 56 | + paymentCreateDto.productId() + "&quantity=" + paymentCreateDto.quantity()); 57 | params.add("cancel_url", 58 | requestUrl + Constant.API_AND_VERSION.getValue() + "/payment/" 59 | + PaymentPgCompany.KAKAO.getName() + "/cancel?paymentId=" + paymentId); 60 | params.add("fail_url", requestUrl + Constant.API_AND_VERSION.getValue() + "/payment/" 61 | + PaymentPgCompany.KAKAO.getName() 62 | + "/fail?paymentId=" + paymentId); 63 | params.add("partner_order_id", String.valueOf(paymentCreateDto.orderId())); 64 | params.add("partner_user_id", String.valueOf(userId)); 65 | params.add("item_name", paymentCreateDto.productName()); 66 | params.add("quantity", String.valueOf(paymentCreateDto.quantity())); 67 | params.add("total_amount", 68 | String.valueOf(paymentCreateDto.totalAmount())); 69 | params.add("tax_free_amount", String.valueOf(paymentCreateDto.taxFreeAmount())); 70 | 71 | return new HttpEntity<>(params, headers); 72 | } 73 | 74 | public HttpEntity> getBodyForApprovePayment(String pgToken, 75 | Long orderId, Long userId, Long paymentId) { 76 | HttpHeaders headers = getHeaders(); 77 | 78 | MultiValueMap params = new LinkedMultiValueMap<>(); 79 | 80 | Payment payment = paymentRepository.findById(paymentId) 81 | .orElseThrow(PaymentNotExistBadRequestException::new); 82 | 83 | params.add("cid", cid); 84 | params.add("tid", payment.getPaymentKey()); 85 | params.add("partner_order_id", String.valueOf(orderId)); 86 | params.add("partner_user_id", String.valueOf(userId)); 87 | params.add("pg_token", pgToken); 88 | params.add("total_amount", String.valueOf(payment.getTotalAmount())); 89 | return new HttpEntity<>(params, headers); 90 | } 91 | 92 | public HttpEntity> getBodyForCancelPayment( 93 | OrderCancelDto orderCancelDto) { 94 | HttpHeaders headers = getHeaders(); 95 | MultiValueMap params = new LinkedMultiValueMap<>(); 96 | 97 | params.add("cid", cid); 98 | params.add("tid", String.valueOf(orderCancelDto.paymentKey())); 99 | params.add("cancel_amount", String.valueOf(orderCancelDto.cancelAmount())); 100 | params.add("cancel_tax_free_amount", String.valueOf(orderCancelDto.cancelTaxFreeAmount())); 101 | return new HttpEntity<>(params, headers); 102 | } 103 | 104 | public HttpEntity> getBodyForOrderDetail(String paymentKey) { 105 | HttpHeaders headers = getHeaders(); 106 | MultiValueMap params = new LinkedMultiValueMap<>(); 107 | params.add("cid", cid); 108 | params.add("tid", String.valueOf(paymentKey)); 109 | return new HttpEntity<>(params, headers); 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/test/java/flab/payment_system/payment/service/PaymentServiceIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.payment.service; 2 | 3 | import flab.payment_system.config.DatabaseCleanUp; 4 | import flab.payment_system.domain.order.dto.OrderDto; 5 | import flab.payment_system.domain.order.dto.OrderProductDto; 6 | import flab.payment_system.domain.payment.dto.PaymentCreateDto; 7 | import flab.payment_system.domain.order.service.OrderService; 8 | import flab.payment_system.domain.payment.entity.Payment; 9 | import flab.payment_system.domain.payment.enums.PaymentPgCompany; 10 | import flab.payment_system.domain.payment.exception.PaymentNotExistBadRequestException; 11 | import flab.payment_system.domain.payment.repository.PaymentRepository; 12 | import flab.payment_system.domain.payment.response.kakao.PaymentKakaoReadyDtoImpl; 13 | import flab.payment_system.domain.payment.response.toss.PaymentTossDtoImpl; 14 | import flab.payment_system.domain.payment.service.PaymentService; 15 | import flab.payment_system.domain.payment.service.kakao.PaymentStrategyKaKaoService; 16 | import flab.payment_system.domain.payment.service.toss.PaymentStrategyTossService; 17 | import flab.payment_system.domain.product.entity.Product; 18 | import flab.payment_system.domain.product.repository.ProductRepository; 19 | import flab.payment_system.domain.user.entity.User; 20 | import flab.payment_system.domain.user.repository.UserRepository; 21 | import org.junit.jupiter.api.*; 22 | import org.springframework.beans.factory.annotation.Autowired; 23 | import org.springframework.beans.factory.annotation.Value; 24 | import org.springframework.boot.test.context.SpringBootTest; 25 | import org.springframework.boot.test.mock.mockito.MockBean; 26 | 27 | import static org.junit.jupiter.api.Assertions.*; 28 | import static org.mockito.ArgumentMatchers.*; 29 | import static org.mockito.Mockito.when; 30 | 31 | @SpringBootTest(properties = {"spring.config.location=classpath:application-test.yml"}) 32 | public class PaymentServiceIntegrationTest { 33 | @MockBean 34 | private PaymentStrategyKaKaoService paymentStrategyKaKaoService; 35 | @MockBean 36 | private PaymentStrategyTossService paymentStrategyTossService; 37 | private final PaymentService paymentService; 38 | private final OrderService orderService; 39 | private final PaymentRepository paymentRepository; 40 | private final ProductRepository productRepository; 41 | private final UserRepository userRepository; 42 | private final DatabaseCleanUp databaseCleanUp; 43 | private final String requestUrl; 44 | private Product product; 45 | private PaymentCreateDto paymentCreateDto; 46 | private Long userId; 47 | 48 | @Autowired 49 | PaymentServiceIntegrationTest(PaymentService paymentService, OrderService orderService, ProductRepository productRepository, UserRepository userRepository, @Value("${test-url}") String requestUrl, PaymentRepository paymentRepository, DatabaseCleanUp databaseCleanUp) { 50 | this.paymentService = paymentService; 51 | this.orderService = orderService; 52 | this.productRepository = productRepository; 53 | this.userRepository = userRepository; 54 | this.requestUrl = requestUrl; 55 | this.paymentRepository = paymentRepository; 56 | this.databaseCleanUp = databaseCleanUp; 57 | 58 | } 59 | 60 | @BeforeEach 61 | void setUp() { 62 | databaseCleanUp.truncateAllEntity(); 63 | product = new Product(); 64 | product.setName("초코파이"); 65 | product.setPrice(5000); 66 | product.setStock(100); 67 | User user = User.builder().email("test@gmail.com").password("1234").build(); 68 | userRepository.save(user); 69 | productRepository.save(product); 70 | 71 | userId = 1L; 72 | Integer quantity = 2; 73 | Integer totalAmount = 5000; 74 | Integer totalFreeAmount = 500; 75 | Integer installMonth = 3; 76 | 77 | OrderProductDto orderProductDto = new OrderProductDto(product.getProductId(), quantity); 78 | OrderDto orderDto = orderService.orderProduct(orderProductDto, userId); 79 | paymentCreateDto = new PaymentCreateDto(orderDto.orderId(), product.getName(), product.getProductId(), quantity, totalAmount, totalFreeAmount, installMonth); 80 | 81 | 82 | when(paymentStrategyKaKaoService.createPayment(eq(paymentCreateDto), eq(userId), eq(requestUrl), anyLong())) 83 | .thenReturn(new PaymentKakaoReadyDtoImpl()); 84 | 85 | when(paymentStrategyTossService.createPayment(eq(paymentCreateDto), eq(userId), eq(requestUrl), anyLong())) 86 | .thenReturn(new PaymentTossDtoImpl()); 87 | 88 | } 89 | 90 | @AfterEach 91 | void tearDown() { 92 | databaseCleanUp.truncateAllEntity(); 93 | } 94 | 95 | @Nested 96 | @DisplayName("결제생성_성공") 97 | public class createPaymentTest { 98 | @DisplayName("카카오 단건결제 생성") 99 | @Test 100 | public void createKakaoPaymentSuccess() { 101 | // given 102 | PaymentPgCompany pgCompany = PaymentPgCompany.KAKAO; 103 | 104 | // when 105 | PaymentKakaoReadyDtoImpl paymentReadyDto = (PaymentKakaoReadyDtoImpl) paymentService.createPayment(paymentCreateDto, requestUrl, userId, pgCompany); 106 | Payment payment = paymentRepository.findById(paymentReadyDto.getPaymentId()).orElseThrow(PaymentNotExistBadRequestException::new); 107 | 108 | // then 109 | assertEquals(paymentReadyDto.getPaymentId(), payment.getPaymentId()); 110 | assertNotNull(payment); 111 | } 112 | 113 | @DisplayName("토스 단건결제 생성") 114 | @Test 115 | public void createTossPaymentSuccess() { 116 | // given 117 | PaymentPgCompany pgCompany = PaymentPgCompany.TOSS; 118 | 119 | // when 120 | PaymentTossDtoImpl paymentReadyDto = (PaymentTossDtoImpl) paymentService.createPayment(paymentCreateDto, requestUrl, userId, pgCompany); 121 | Payment payment = paymentRepository.findById(paymentReadyDto.getPaymentId()).orElseThrow(PaymentNotExistBadRequestException::new); 122 | 123 | // then 124 | assertEquals(paymentReadyDto.getPaymentId(), payment.getPaymentId()); 125 | assertNotNull(payment); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/payment/service/PaymentService.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.payment.service; 2 | 3 | import flab.payment_system.adapter.PaymentAdapter; 4 | import flab.payment_system.domain.order.dto.OrderCancelDto; 5 | import flab.payment_system.domain.payment.dto.PaymentCreateDto; 6 | import flab.payment_system.domain.payment.entity.Payment; 7 | import flab.payment_system.domain.payment.enums.PaymentPgCompany; 8 | import flab.payment_system.domain.payment.enums.PaymentStateConstant; 9 | import flab.payment_system.domain.payment.exception.PaymentAlreadyApprovedConflictException; 10 | import flab.payment_system.domain.payment.exception.PaymentNotApprovedConflictException; 11 | import flab.payment_system.domain.payment.exception.PaymentNotExistBadRequestException; 12 | import flab.payment_system.domain.payment.repository.PaymentRepository; 13 | import flab.payment_system.domain.payment.response.PaymentApprovalDto; 14 | import flab.payment_system.domain.payment.response.PaymentCancelDto; 15 | import flab.payment_system.domain.payment.response.PaymentOrderDetailDto; 16 | import flab.payment_system.domain.payment.response.PaymentReadyDto; 17 | import flab.payment_system.domain.payment.response.toss.Settlement; 18 | import lombok.RequiredArgsConstructor; 19 | 20 | import org.springframework.stereotype.Service; 21 | import org.springframework.transaction.annotation.Transactional; 22 | 23 | import java.util.Map; 24 | import java.util.Objects; 25 | import java.util.Optional; 26 | 27 | @Service 28 | @RequiredArgsConstructor 29 | public class PaymentService { 30 | 31 | private final Map paymentStrategies; 32 | private final PaymentRepository paymentRepository; 33 | private final PaymentAdapter paymentAdapter; 34 | 35 | private PaymentStrategy getStrategy(PaymentPgCompany paymentPgCompany) { 36 | return paymentStrategies.get(paymentPgCompany); 37 | } 38 | 39 | @Transactional 40 | public PaymentReadyDto createPayment(PaymentCreateDto paymentCreateDto, 41 | String requestUrl, Long userId, PaymentPgCompany pgCompany) { 42 | PaymentStrategy paymentStrategy = getStrategy(pgCompany); 43 | 44 | Optional optionalPayment = paymentRepository.findByOrderProduct_OrderId(paymentCreateDto.orderId()); 45 | 46 | Payment payment = optionalPayment.orElseGet(() -> paymentRepository.save( 47 | Payment.builder() 48 | .orderProduct(paymentAdapter.getOrderProductByOrderId(paymentCreateDto.orderId())) 49 | .state(PaymentStateConstant.ONGOING.getValue()) 50 | .pgCompany(pgCompany.getValue()) 51 | .totalAmount(paymentCreateDto.totalAmount()) 52 | .taxFreeAmount(paymentCreateDto.taxFreeAmount()) 53 | .installMonth(paymentCreateDto.installMonth()) 54 | .build())); 55 | 56 | if (Objects.equals(payment.getState(), PaymentStateConstant.APPROVED.getValue())) 57 | throw new PaymentAlreadyApprovedConflictException(); 58 | 59 | PaymentReadyDto paymentReadyDto = paymentStrategy.createPayment(paymentCreateDto, userId, 60 | requestUrl, payment.getPaymentId()); 61 | 62 | paymentReadyDto.setPaymentId(payment.getPaymentId()); 63 | 64 | payment.setPaymentKey(paymentReadyDto.getPaymentKey()); 65 | 66 | return paymentReadyDto; 67 | } 68 | 69 | @Transactional 70 | public PaymentApprovalDto approvePayment(String pgToken, Long orderId, Long userId, Long paymentId, Long productId, 71 | Integer quantity, PaymentPgCompany pgCompany) { 72 | PaymentStrategy paymentStrategy = getStrategy(pgCompany); 73 | 74 | PaymentApprovalDto paymentApprovalDto = paymentStrategy.approvePayment(pgToken, orderId, 75 | userId, paymentId); 76 | 77 | Payment payment = paymentRepository.findById(paymentId).orElseThrow(PaymentNotExistBadRequestException::new); 78 | if (payment.getState().equals(PaymentStateConstant.APPROVED.getValue())) 79 | throw new PaymentAlreadyApprovedConflictException(); 80 | 81 | paymentRepository.updatePaymentStateByPaymentId(paymentId, 82 | PaymentStateConstant.APPROVED.getValue()); 83 | 84 | paymentAdapter.decreaseStock(productId, quantity); 85 | 86 | return paymentApprovalDto; 87 | } 88 | 89 | @Transactional 90 | public void failPayment(Long paymentId) { 91 | Payment payment = paymentRepository.findById(paymentId).orElseThrow(PaymentNotExistBadRequestException::new); 92 | 93 | if (payment.getState().equals(PaymentStateConstant.APPROVED.getValue())) 94 | throw new PaymentAlreadyApprovedConflictException(); 95 | 96 | paymentRepository.updatePaymentStateByPaymentId(paymentId, 97 | PaymentStateConstant.FAIL.getValue()); 98 | } 99 | 100 | @Transactional 101 | public PaymentCancelDto cancelPayment(OrderCancelDto orderCancelDto, PaymentPgCompany pgCompany) { 102 | PaymentStrategy paymentStrategy = getStrategy(pgCompany); 103 | 104 | Payment payment = paymentRepository.findByOrderProduct_OrderId(orderCancelDto.orderId()) 105 | .orElseThrow(PaymentNotExistBadRequestException::new); 106 | if (!payment.getState().equals(PaymentStateConstant.APPROVED.getValue())) 107 | throw new PaymentNotApprovedConflictException(); 108 | 109 | PaymentCancelDto paymentCancelDto = paymentStrategy.cancelPayment(orderCancelDto); 110 | 111 | paymentRepository.updatePaymentStateByOrderId(orderCancelDto.orderId(), 112 | PaymentStateConstant.CANCEL.getValue()); 113 | 114 | paymentAdapter.increaseStock(orderCancelDto.productId(), orderCancelDto.quantity()); 115 | 116 | return paymentCancelDto; 117 | } 118 | 119 | public PaymentOrderDetailDto getOrderDetail(String paymentKey, PaymentPgCompany pgCompany) { 120 | PaymentStrategy paymentStrategy = getStrategy(pgCompany); 121 | return paymentStrategy.getOrderDetail(paymentKey); 122 | } 123 | 124 | public Payment getPaymentByPaymentId(Long paymentId) { 125 | return paymentRepository.findById(paymentId).orElseThrow(PaymentNotExistBadRequestException::new); 126 | } 127 | 128 | public Settlement[] getSettlementList(PaymentPgCompany pgCompany) { 129 | PaymentStrategy paymentStrategy = getStrategy(pgCompany); 130 | return paymentStrategy.getSettlementList(); 131 | } 132 | } 133 | 134 | -------------------------------------------------------------------------------- /src/test/java/flab/payment_system/batch/compensation/BatchIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.batch.compensation; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.*; 6 | 7 | import flab.payment_system.adapter.PaymentAdapter; 8 | import flab.payment_system.domain.order.entity.OrderProduct; 9 | import flab.payment_system.domain.order.repository.OrderRepository; 10 | import flab.payment_system.domain.payment.entity.Payment; 11 | import flab.payment_system.domain.payment.enums.PaymentStateConstant; 12 | import flab.payment_system.domain.payment.response.toss.Settlement; 13 | import flab.payment_system.domain.payment.service.PaymentService; 14 | import flab.payment_system.domain.payment.service.PaymentStrategy; 15 | import flab.payment_system.domain.product.entity.Product; 16 | import flab.payment_system.domain.product.repository.ProductRepository; 17 | import flab.payment_system.domain.user.entity.User; 18 | import flab.payment_system.domain.user.repository.UserRepository; 19 | import org.junit.jupiter.api.BeforeEach; 20 | import org.junit.jupiter.api.Test; 21 | import org.springframework.batch.core.Job; 22 | import org.springframework.batch.core.JobExecution; 23 | import org.springframework.batch.core.JobParameters; 24 | import org.springframework.batch.core.JobParametersBuilder; 25 | import org.springframework.batch.core.launch.JobLauncher; 26 | import org.springframework.beans.factory.annotation.Autowired; 27 | import org.springframework.boot.test.context.SpringBootTest; 28 | import org.springframework.boot.test.context.TestConfiguration; 29 | import org.springframework.context.annotation.Bean; 30 | import org.springframework.context.annotation.Import; 31 | import org.springframework.context.annotation.Primary; 32 | import org.springframework.context.annotation.Profile; 33 | 34 | import flab.payment_system.domain.payment.enums.PaymentPgCompany; 35 | import flab.payment_system.domain.payment.repository.PaymentRepository; 36 | import org.springframework.stereotype.Component; 37 | 38 | @Import(value = BatchIntegrationTest.PaymentServiceTestConfig.class) 39 | @Profile("test") 40 | @SpringBootTest(properties = {"spring.config.location=classpath:application-test.yml"}) 41 | public class BatchIntegrationTest { 42 | @TestConfiguration 43 | static class PaymentServiceTestConfig { 44 | @Autowired 45 | private Map paymentStrategies; 46 | 47 | @Autowired 48 | private PaymentAdapter paymentAdapter; 49 | 50 | @Autowired 51 | private PaymentRepository paymentRepository; 52 | 53 | 54 | @Bean 55 | @Primary 56 | public PaymentService paymentServiceStub() { 57 | return new PaymentServiceStub(paymentStrategies, paymentRepository, paymentAdapter); 58 | } 59 | } 60 | 61 | // 내부 Stub 62 | private static class PaymentServiceStub extends PaymentService { 63 | 64 | private Settlement[] stubSettlementList; 65 | 66 | public PaymentServiceStub(Map paymentStrategies, PaymentRepository paymentRepository, PaymentAdapter paymentAdapter) { 67 | super(paymentStrategies, paymentRepository, paymentAdapter); 68 | } 69 | 70 | public void setStubSettlementList(Settlement[] stubSettlementList) { 71 | this.stubSettlementList = stubSettlementList; 72 | } 73 | 74 | @Override 75 | public Settlement[] getSettlementList(PaymentPgCompany pgCompany) { 76 | System.out.println("hello:"+stubSettlementList[0]); 77 | return stubSettlementList; 78 | } 79 | } 80 | 81 | @Autowired 82 | private JobLauncher jobLauncher; 83 | 84 | @Autowired 85 | private Job paymentSyncJob; 86 | 87 | @Autowired 88 | private PaymentRepository paymentRepository; 89 | 90 | @Autowired 91 | private OrderRepository orderRepository; 92 | 93 | private Payment payment; 94 | @Autowired 95 | private ProductRepository productRepository; 96 | 97 | @Autowired 98 | private UserRepository userRepository; 99 | 100 | @Autowired 101 | private PaymentService paymentService; 102 | 103 | @BeforeEach 104 | void setup() { 105 | Product product = new Product(); 106 | User user = User.builder().email("testuser@example.com").build(); 107 | OrderProduct orderProduct = OrderProduct.builder().user(user).product(product).quantity(1).build(); 108 | payment = Payment.builder().orderProduct(orderProduct).state(PaymentStateConstant.FAIL.getValue()).pgCompany(PaymentPgCompany.TOSS.getValue()).totalAmount(5000).taxFreeAmount(0).installMonth(0).paymentKey("paymentKey1").build(); 109 | 110 | productRepository.save(product); 111 | userRepository.save(user); 112 | orderRepository.save(orderProduct); 113 | paymentRepository.save(payment); 114 | 115 | PaymentServiceStub paymentServiceStub = (PaymentServiceStub) paymentService; // 주입된 paymentService를 Stub으로 캐스팅 116 | paymentServiceStub.setStubSettlementList(createStubSettlements()); 117 | } 118 | 119 | @Test 120 | void testPaymentSyncJob() throws Exception { 121 | // 배치 실행 122 | JobParameters jobParameters = new JobParametersBuilder().addLong("time", System.currentTimeMillis()).toJobParameters(); 123 | 124 | JobExecution jobExecution = jobLauncher.run(paymentSyncJob, jobParameters); 125 | 126 | // 배치 작업이 성공적으로 완료되었는지 확인 127 | assertThat(jobExecution.getStatus().isUnsuccessful()).isFalse(); 128 | assertThat(jobExecution.getExitStatus().getExitCode()).isEqualTo("COMPLETED"); 129 | 130 | // 배치 작업 후 Payment 상태가 APPROVED로 변경되었는지 확인 131 | Optional updatedPayment = paymentRepository.findById(payment.getPaymentId()); 132 | assertThat(updatedPayment).isPresent(); 133 | assertThat(updatedPayment.get().getState()).isEqualTo(PaymentStateConstant.APPROVED.getValue()); 134 | } 135 | 136 | private Settlement[] createStubSettlements() { 137 | List settlements = new ArrayList<>(); 138 | 139 | // 첫 번째 가짜 Settlement 데이터 140 | Settlement stubSettlement = new Settlement(); 141 | stubSettlement.setOrderId(String.valueOf(payment.getOrderProduct().getOrderId())); 142 | stubSettlement.setPaymentKey(payment.getPaymentKey()); 143 | stubSettlement.setAmount(5000); 144 | stubSettlement.setApprovedAt("2024-10-12T12:00:00"); 145 | settlements.add(stubSettlement); 146 | 147 | return settlements.toArray(new Settlement[0]); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/flab/payment_system/domain/user/service/UserService.java: -------------------------------------------------------------------------------- 1 | package flab.payment_system.domain.user.service; 2 | 3 | import flab.payment_system.adapter.UserAdapter; 4 | import flab.payment_system.domain.user.entity.User; 5 | import flab.payment_system.domain.user.entity.UserVerification; 6 | import flab.payment_system.domain.user.dto.UserConfirmVerificationNumberDto; 7 | import flab.payment_system.domain.user.dto.UserDto; 8 | import flab.payment_system.domain.user.dto.UserSignUpDto; 9 | import flab.payment_system.domain.user.dto.UserVerificationDto; 10 | import flab.payment_system.domain.user.dto.UserVerifyEmailDto; 11 | import flab.payment_system.domain.user.exception.*; 12 | import flab.payment_system.domain.user.repository.UserRepository; 13 | import flab.payment_system.domain.user.repository.UserVerificationRepository; 14 | import jakarta.servlet.http.HttpSession; 15 | 16 | import java.util.Optional; 17 | import java.util.concurrent.ThreadLocalRandom; 18 | 19 | import lombok.RequiredArgsConstructor; 20 | import org.springframework.security.crypto.password.PasswordEncoder; 21 | import org.springframework.stereotype.Service; 22 | import org.springframework.transaction.annotation.Transactional; 23 | 24 | @Service 25 | @RequiredArgsConstructor 26 | public class UserService { 27 | 28 | private final UserAdapter userAdapter; 29 | 30 | private final UserRepository userRepository; 31 | private final UserVerificationRepository userVerificationRepository; 32 | private final PasswordEncoder passwordEncoder; 33 | 34 | @Transactional 35 | public void signUpUser(UserSignUpDto userSignUpDto) { 36 | Optional optionalUser = userRepository.findByEmail(userSignUpDto.email()); 37 | if (optionalUser.isPresent()) { 38 | throw new UserEmailAlreadyExistConflictException(); 39 | } 40 | 41 | confirmUserIsAuthorized(userSignUpDto); 42 | 43 | boolean isPasswordConfirmed = comparePasswordAndConfirmPassword( 44 | passwordEncoder.encode(userSignUpDto.password()), userSignUpDto.confirmPassword()); 45 | 46 | if (!isPasswordConfirmed) { 47 | throw new UserSignUpBadRequestException(); 48 | } 49 | 50 | userRepository.save(User.builder() 51 | .email(userSignUpDto.email()) 52 | .password(passwordEncoder.encode(userSignUpDto.password())).build()); 53 | 54 | 55 | } 56 | 57 | private boolean comparePasswordAndConfirmPassword(String hashedPassword, 58 | String comparedPassword) { 59 | return passwordEncoder.matches(comparedPassword, hashedPassword); 60 | } 61 | 62 | @Transactional(readOnly = true) 63 | public void confirmUserIsAuthorized(UserSignUpDto userSignUpDto) { 64 | Optional optionalUserVerification = userVerificationRepository.findById( 65 | userSignUpDto.verificationId()); 66 | 67 | UserVerification userVerification = optionalUserVerification.orElseThrow( 68 | UserVerificationIdBadRequestException::new); 69 | 70 | if (!userVerification.getEmail().equals(userSignUpDto.email())) { 71 | throw new UserVerificationEmailBadRequestException(); 72 | } 73 | 74 | if (!userVerification.isVerified()) { 75 | throw new UserVerificationUnauthorizedException(); 76 | } 77 | } 78 | 79 | @Transactional 80 | public UserVerificationDto verifyUserEmail(UserVerifyEmailDto userVerifyEmailDto) { 81 | Optional optionalUser = userRepository.findByEmail(userVerifyEmailDto.email()); 82 | 83 | if (optionalUser.isPresent()) { 84 | throw new UserEmailAlreadyExistConflictException(); 85 | } 86 | 87 | int verificationNumber = sendVerificationNumberToUserEmail(userVerifyEmailDto); 88 | 89 | UserVerification userVerification = userVerificationRepository.save( 90 | UserVerification.builder() 91 | .email(userVerifyEmailDto.email()) 92 | .verificationNumber(verificationNumber).isVerified(false).build()); 93 | 94 | return new UserVerificationDto( 95 | userVerification.getVerificationId(), verificationNumber, 96 | userVerification.getEmail(), userVerification.isVerified()); 97 | } 98 | 99 | public int sendVerificationNumberToUserEmail(UserVerifyEmailDto userVerifyEmailDto) { 100 | ThreadLocalRandom random = ThreadLocalRandom.current(); 101 | 102 | int verificationNumber = (random.nextInt(900000) + 100000) % 1000000; 103 | 104 | userAdapter.sendMail(userVerifyEmailDto.email(), 105 | "[payment_system] 회원가입을 위한 인증번호 메일입니다.", 106 | userAdapter.setContextForSendValidationNumberForSendMail(String.valueOf(verificationNumber))); 107 | 108 | return verificationNumber; 109 | } 110 | 111 | 112 | @Transactional 113 | public boolean confirmVerificationNumber( 114 | UserConfirmVerificationNumberDto userConfirmVerificationNumberDto) { 115 | Optional optionalUserVerification = userVerificationRepository.findById( 116 | userConfirmVerificationNumberDto.verificationId()); 117 | 118 | UserVerification userVerification = optionalUserVerification.orElseThrow( 119 | UserVerificationIdBadRequestException::new); 120 | 121 | if (!(userVerification.getEmail().equals(userConfirmVerificationNumberDto.email()))) { 122 | throw new UserVerificationEmailBadRequestException(); 123 | } 124 | 125 | if (!(userVerification.getVerificationNumber() 126 | .equals(userConfirmVerificationNumberDto.verificationNumber()))) { 127 | throw new UserVerificationNumberBadRequestException(); 128 | } 129 | 130 | userVerificationRepository.save( 131 | UserVerification.builder() 132 | .verificationId(userConfirmVerificationNumberDto.verificationId()) 133 | .email(userConfirmVerificationNumberDto.email()) 134 | .verificationNumber(userConfirmVerificationNumberDto.verificationNumber()) 135 | .isVerified(true).build()); 136 | 137 | return true; 138 | } 139 | 140 | @Transactional(readOnly = true) 141 | public void signInUser(UserDto userDto, HttpSession session) { 142 | if (userAdapter.getUserId(session).isPresent()) { 143 | throw new UserAlreadySignInConflictException(); 144 | } 145 | 146 | Optional optionalUser = userRepository.findByEmail(userDto.email()); 147 | 148 | User user = optionalUser.orElseThrow(UserEmailNotExistBadRequestException::new); 149 | 150 | boolean isPasswordConfirmed = comparePasswordAndConfirmPassword(user.getPassword(), 151 | userDto.password()); 152 | 153 | if (!isPasswordConfirmed) { 154 | throw new UserPasswordFailBadRequestException(); 155 | } 156 | 157 | userAdapter.setUserId(session, user.getUserId()); 158 | } 159 | 160 | public void signOutUser(HttpSession session) { 161 | getUserId(session); 162 | userAdapter.invalidate(session); 163 | } 164 | 165 | public Long getUserId(HttpSession session) { 166 | return userAdapter.getUserId(session).orElseThrow(UserNotSignInedConflictException::new); 167 | } 168 | 169 | @Transactional(readOnly = true) 170 | public User getUserByUserId(Long userId) { 171 | return userRepository.findById(userId).orElseThrow(UserNotExistBadRequestException::new); 172 | } 173 | } 174 | --------------------------------------------------------------------------------