From c760d66cf6b8e1238bb46382ea7f53a4b1019c15 Mon Sep 17 00:00:00 2001 From: Kim DongHyo <60608509+kdhyo@users.noreply.github.com> Date: Fri, 15 Jul 2022 04:32:20 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=98=81=ED=99=94=ED=8B=B0=EC=BC=93=20?= =?UTF-8?q?=EC=98=88=EB=A7=A4=20=EA=B5=AC=ED=98=84=20(=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=ED=8E=98=EC=9D=B4API=20=EC=97=B0=EB=8F=99)=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 영화티켓 예매 구현 (카카오페이API 연동) * fix: 테스트 값 제거 --- server/build.gradle.kts | 10 ++ .../ticketing/server/ServerApplication.java | 5 +- .../dto}/SequenceGenerator.java | 2 +- .../domain => global/dto}/Snowflake.java | 2 +- .../server/global/exception/ErrorCode.java | 29 ++++- .../exception/GlobalExceptionHandler.java | 2 - .../server/global/redis/PaymentCache.java | 44 +++++++ .../global/redis/PaymentCacheRepository.java | 10 ++ .../server/global/redis/RedisConfig.java | 21 ++++ .../global/security/WebSecurityConfig.java | 3 +- .../constraints/NotEmptyCollection.java | 22 ++++ .../NotEmptyCollectionValidator.java | 20 ++++ .../movie/application/TicketController.java | 6 +- .../request/TicketCancelRequest.java | 13 ++ .../request/TicketReservationRequest.java | 15 +++ .../request/TicketSoldRequest.java | 19 +++ .../response/TicketCancelResponse.java | 13 ++ .../response/TicketReservationResponse.java | 25 ++++ .../response/TicketSoldResponse.java | 15 +++ .../ticketing/server/movie/domain/Ticket.java | 33 +++++- .../server/movie/domain/TicketStatus.java | 7 +- .../domain/repository/TicketRepository.java | 11 ++ .../movie/service/TicketServiceImpl.java | 68 +++++++++++ .../service/dto/TicketReservationDTO.java | 19 +++ .../movie/service/dto/TicketSoldDTO.java | 19 +++ .../movie/service/dto/TicketsCancelDTO.java | 18 +++ .../service/dto/TicketsReservationDTO.java | 19 +++ .../movie/service/dto/TicketsSoldDTO.java | 19 +++ .../service/interfaces/TicketService.java | 11 ++ .../server/movie/setup/MovieSetupService.java | 14 ++- .../payment/api/CoreFeignConfiguration.java | 20 ++++ .../server/payment/api/KakaoPayClient.java | 26 ++++ .../payment/api/KakaoPayProperties.java | 40 +++++++ .../server/payment/api/MovieClient.java | 11 ++ .../payment/api/dto/SnakeCaseStrategy.java | 12 ++ .../dto/requset/KakaoPayApproveRequest.java | 50 ++++++++ .../api/dto/requset/KakaoPayReadyRequest.java | 92 ++++++++++++++ .../payment/api/dto/response/Amount.java | 27 +++++ .../payment/api/dto/response/CardInfo.java | 45 +++++++ .../dto/response/KakaoPayApproveResponse.java | 51 ++++++++ .../dto/response/KakaoPayReadyResponse.java | 37 ++++++ .../payment/api/impl/MovieClientImpl.java | 27 +++++ .../application/PaymentController.java | 41 +++++++ .../request/PaymentReadyRequest.java | 13 ++ .../response/PaymentCancelResponse.java | 16 +++ .../response/PaymentCompleteResponse.java | 16 +++ .../server/payment/domain/Payment.java | 20 +++- .../server/payment/domain/PaymentStatus.java | 12 +- .../service/PaymentApisServiceImpl.java | 112 ++++++++++++++++++ .../payment/service/dto/CreatePaymentDTO.java | 32 ----- .../payment/service/dto/PaymentCancelDTO.java | 32 +++++ .../service/dto/PaymentCompleteDTO.java | 33 ++++++ .../payment/service/dto/PaymentReadyDTO.java | 40 +++++++ .../interfaces/PaymentApisService.java | 13 ++ .../user/application/UserController.java | 1 + .../UserChangeGradeResponse.java | 2 +- .../server/user/domain/ChangeGradeDTO.java | 2 +- .../server/user/service/UserServiceImpl.java | 2 +- .../src/main/resources/application-local.yml | 10 ++ server/src/main/resources/application.yml | 8 ++ .../server/movie/domain/TicketTest.java | 106 +++++++++++++++++ .../movie/service/TicketServiceImplTest.java | 36 ++++++ .../server/payment/domain/PaymentTest.java | 2 +- .../service/PaymentServiceImplTest.java | 32 ++++- .../server/user/domain/SnowflakeTest.java | 1 + .../user/service/UserApisServiceImplTest.java | 35 +++++- .../user/service/UserServiceImplTest.java | 2 +- 67 files changed, 1507 insertions(+), 64 deletions(-) rename server/src/main/java/com/ticketing/server/{user/domain => global/dto}/SequenceGenerator.java (67%) rename server/src/main/java/com/ticketing/server/{user/domain => global/dto}/Snowflake.java (98%) create mode 100644 server/src/main/java/com/ticketing/server/global/redis/PaymentCache.java create mode 100644 server/src/main/java/com/ticketing/server/global/redis/PaymentCacheRepository.java create mode 100644 server/src/main/java/com/ticketing/server/global/validator/constraints/NotEmptyCollection.java create mode 100644 server/src/main/java/com/ticketing/server/global/validator/constraintvalidators/NotEmptyCollectionValidator.java create mode 100644 server/src/main/java/com/ticketing/server/movie/application/request/TicketCancelRequest.java create mode 100644 server/src/main/java/com/ticketing/server/movie/application/request/TicketReservationRequest.java create mode 100644 server/src/main/java/com/ticketing/server/movie/application/request/TicketSoldRequest.java create mode 100644 server/src/main/java/com/ticketing/server/movie/application/response/TicketCancelResponse.java create mode 100644 server/src/main/java/com/ticketing/server/movie/application/response/TicketReservationResponse.java create mode 100644 server/src/main/java/com/ticketing/server/movie/application/response/TicketSoldResponse.java create mode 100644 server/src/main/java/com/ticketing/server/movie/service/dto/TicketReservationDTO.java create mode 100644 server/src/main/java/com/ticketing/server/movie/service/dto/TicketSoldDTO.java create mode 100644 server/src/main/java/com/ticketing/server/movie/service/dto/TicketsCancelDTO.java create mode 100644 server/src/main/java/com/ticketing/server/movie/service/dto/TicketsReservationDTO.java create mode 100644 server/src/main/java/com/ticketing/server/movie/service/dto/TicketsSoldDTO.java create mode 100644 server/src/main/java/com/ticketing/server/payment/api/CoreFeignConfiguration.java create mode 100644 server/src/main/java/com/ticketing/server/payment/api/KakaoPayClient.java create mode 100644 server/src/main/java/com/ticketing/server/payment/api/KakaoPayProperties.java create mode 100644 server/src/main/java/com/ticketing/server/payment/api/dto/SnakeCaseStrategy.java create mode 100644 server/src/main/java/com/ticketing/server/payment/api/dto/requset/KakaoPayApproveRequest.java create mode 100644 server/src/main/java/com/ticketing/server/payment/api/dto/requset/KakaoPayReadyRequest.java create mode 100644 server/src/main/java/com/ticketing/server/payment/api/dto/response/Amount.java create mode 100644 server/src/main/java/com/ticketing/server/payment/api/dto/response/CardInfo.java create mode 100644 server/src/main/java/com/ticketing/server/payment/api/dto/response/KakaoPayApproveResponse.java create mode 100644 server/src/main/java/com/ticketing/server/payment/api/dto/response/KakaoPayReadyResponse.java create mode 100644 server/src/main/java/com/ticketing/server/payment/application/request/PaymentReadyRequest.java create mode 100644 server/src/main/java/com/ticketing/server/payment/application/response/PaymentCancelResponse.java create mode 100644 server/src/main/java/com/ticketing/server/payment/application/response/PaymentCompleteResponse.java delete mode 100644 server/src/main/java/com/ticketing/server/payment/service/dto/CreatePaymentDTO.java create mode 100644 server/src/main/java/com/ticketing/server/payment/service/dto/PaymentCancelDTO.java create mode 100644 server/src/main/java/com/ticketing/server/payment/service/dto/PaymentCompleteDTO.java create mode 100644 server/src/main/java/com/ticketing/server/payment/service/dto/PaymentReadyDTO.java rename server/src/main/java/com/ticketing/server/user/application/{ => response}/UserChangeGradeResponse.java (82%) diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 779f928..98ce4ae 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -23,6 +23,8 @@ repositories { mavenCentral() } +extra["springCloudVersion"] = "2021.0.3" + dependencies { implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-security") @@ -39,6 +41,8 @@ dependencies { implementation("com.googlecode.json-simple:json-simple:1.1.1") implementation("org.springframework.boot:spring-boot-starter-data-redis") implementation("com.google.code.findbugs:jsr305:3.0.2") + implementation ("org.springframework.cloud:spring-cloud-starter-config") + implementation("org.springframework.cloud:spring-cloud-starter-openfeign") modules { module("org.springframework.boot:spring-boot-starter-logging") { @@ -58,6 +62,12 @@ dependencies { testImplementation("org.springframework.security:spring-security-test") } +dependencyManagement { + imports { + mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}") + } +} + tasks.withType { useJUnitPlatform() } diff --git a/server/src/main/java/com/ticketing/server/ServerApplication.java b/server/src/main/java/com/ticketing/server/ServerApplication.java index caa59da..6279654 100644 --- a/server/src/main/java/com/ticketing/server/ServerApplication.java +++ b/server/src/main/java/com/ticketing/server/ServerApplication.java @@ -1,14 +1,17 @@ package com.ticketing.server; import com.ticketing.server.global.security.jwt.JwtProperties; +import com.ticketing.server.payment.api.KakaoPayProperties; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @EnableJpaAuditing +@EnableFeignClients @SpringBootApplication -@EnableConfigurationProperties(JwtProperties.class) +@EnableConfigurationProperties({JwtProperties.class, KakaoPayProperties.class}) public class ServerApplication { public static void main(String[] args) { diff --git a/server/src/main/java/com/ticketing/server/user/domain/SequenceGenerator.java b/server/src/main/java/com/ticketing/server/global/dto/SequenceGenerator.java similarity index 67% rename from server/src/main/java/com/ticketing/server/user/domain/SequenceGenerator.java rename to server/src/main/java/com/ticketing/server/global/dto/SequenceGenerator.java index 82cce95..7da2f88 100644 --- a/server/src/main/java/com/ticketing/server/user/domain/SequenceGenerator.java +++ b/server/src/main/java/com/ticketing/server/global/dto/SequenceGenerator.java @@ -1,4 +1,4 @@ -package com.ticketing.server.user.domain; +package com.ticketing.server.global.dto; public interface SequenceGenerator { diff --git a/server/src/main/java/com/ticketing/server/user/domain/Snowflake.java b/server/src/main/java/com/ticketing/server/global/dto/Snowflake.java similarity index 98% rename from server/src/main/java/com/ticketing/server/user/domain/Snowflake.java rename to server/src/main/java/com/ticketing/server/global/dto/Snowflake.java index f120057..189bcfc 100644 --- a/server/src/main/java/com/ticketing/server/user/domain/Snowflake.java +++ b/server/src/main/java/com/ticketing/server/global/dto/Snowflake.java @@ -1,4 +1,4 @@ -package com.ticketing.server.user.domain; +package com.ticketing.server.global.dto; import java.net.NetworkInterface; import java.security.SecureRandom; diff --git a/server/src/main/java/com/ticketing/server/global/exception/ErrorCode.java b/server/src/main/java/com/ticketing/server/global/exception/ErrorCode.java index 79ce82e..5b8f148 100644 --- a/server/src/main/java/com/ticketing/server/global/exception/ErrorCode.java +++ b/server/src/main/java/com/ticketing/server/global/exception/ErrorCode.java @@ -5,6 +5,7 @@ import static org.springframework.http.HttpStatus.CONFLICT; import static org.springframework.http.HttpStatus.FORBIDDEN; import static org.springframework.http.HttpStatus.NOT_FOUND; +import com.ticketing.server.global.redis.PaymentCache; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.http.HttpStatus; @@ -18,6 +19,11 @@ public enum ErrorCode { TOKEN_TYPE(BAD_REQUEST, "토큰 타입이 올바르지 않습니다."), UNAVAILABLE_REFRESH_TOKEN(BAD_REQUEST, "사용할 수 없는 토큰 입니다."), UNABLE_CHANGE_GRADE(BAD_REQUEST, "동일한 등급으로 변경할 수 없습니다."), + INVALID_TICKET_ID(BAD_REQUEST, "잘못된 티켓 ID가 존재합니다."), + BAD_REQUEST_MOVIE_TIME(BAD_REQUEST, "동일한 상영시간만 결제 가능합니다."), + BAD_REQUEST_PAYMENT_COMPLETE(BAD_REQUEST, "처리할 결제 정보가 존재하지 않습니다."), + BAD_REQUEST_PAYMENT_READY(BAD_REQUEST, "이미 진행 중인 결제가 존재합니다."), + BAD_REQUEST_PAYMENT_CANCEL(BAD_REQUEST, "취소할 티켓이 존재하지 않습니다."), /* 403 FORBIDDEN : 접근 권한 제한 */ VALID_USER_ID(FORBIDDEN, "해당 정보에 접근 권한이 존재하지 않습니다."), @@ -39,7 +45,7 @@ public enum ErrorCode { DELETED_MOVIE(CONFLICT, "이미 삭제된 영화 입니다."); private final HttpStatus httpStatus; - private final String detail; + private String detail; /* 400 BAD_REQUEST : 잘못된 요청 */ public static TicketingException throwMismatchPassword() { @@ -58,6 +64,26 @@ public enum ErrorCode { throw new TicketingException(UNABLE_CHANGE_GRADE); } + public static TicketingException throwInvalidTicketId() { + throw new TicketingException(INVALID_TICKET_ID); + } + + public static TicketingException throwBadRequestMovieTime() { + throw new TicketingException(BAD_REQUEST_MOVIE_TIME); + } + + public static TicketingException throwBadRequestPaymentComplete() { + throw new TicketingException(BAD_REQUEST_PAYMENT_COMPLETE); + } + + public static TicketingException throwBadRequestPaymentReady() { + throw new TicketingException(BAD_REQUEST_PAYMENT_READY); + } + + public static TicketingException throwBadRequestPaymentCancel() { + throw new TicketingException(BAD_REQUEST_PAYMENT_CANCEL); + } + /* 403 FORBIDDEN : 접근 권한 제한 */ public static TicketingException throwValidUserId() { throw new TicketingException(VALID_USER_ID); @@ -113,4 +139,5 @@ public enum ErrorCode { throw new TicketingException(DELETED_MOVIE); } + } diff --git a/server/src/main/java/com/ticketing/server/global/exception/GlobalExceptionHandler.java b/server/src/main/java/com/ticketing/server/global/exception/GlobalExceptionHandler.java index 928c017..dcab64d 100644 --- a/server/src/main/java/com/ticketing/server/global/exception/GlobalExceptionHandler.java +++ b/server/src/main/java/com/ticketing/server/global/exception/GlobalExceptionHandler.java @@ -9,7 +9,6 @@ import static org.springframework.http.HttpStatus.UNAUTHORIZED; import static org.springframework.http.HttpStatus.UNSUPPORTED_MEDIA_TYPE; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Set; @@ -24,7 +23,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.AuthenticationException; import org.springframework.validation.BindException; -import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; diff --git a/server/src/main/java/com/ticketing/server/global/redis/PaymentCache.java b/server/src/main/java/com/ticketing/server/global/redis/PaymentCache.java new file mode 100644 index 0000000..d8557dd --- /dev/null +++ b/server/src/main/java/com/ticketing/server/global/redis/PaymentCache.java @@ -0,0 +1,44 @@ +package com.ticketing.server.global.redis; + +import java.util.List; +import javax.persistence.Column; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.index.Indexed; + +@Getter +@RedisHash("Payment") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PaymentCache { + + @Id + @GeneratedValue + @Column(name = "payment_ready_id") + private Long id; + + @Indexed + private String email; + + private String movieTitle; + + private String tid; + + private List ticketIds; + + private Long userAlternateId; + + private Long paymentNumber; + + public PaymentCache(String email, String movieTitle, String tid, List ticketIds, Long userAlternateId, Long paymentNumber) { + this.email = email; + this.movieTitle = movieTitle; + this.tid = tid; + this.ticketIds = ticketIds; + this.userAlternateId = userAlternateId; + this.paymentNumber = paymentNumber; + } +} diff --git a/server/src/main/java/com/ticketing/server/global/redis/PaymentCacheRepository.java b/server/src/main/java/com/ticketing/server/global/redis/PaymentCacheRepository.java new file mode 100644 index 0000000..2c2de1a --- /dev/null +++ b/server/src/main/java/com/ticketing/server/global/redis/PaymentCacheRepository.java @@ -0,0 +1,10 @@ +package com.ticketing.server.global.redis; + +import java.util.Optional; +import org.springframework.data.repository.CrudRepository; + +public interface PaymentCacheRepository extends CrudRepository { + + Optional findByEmail(String email); + +} diff --git a/server/src/main/java/com/ticketing/server/global/redis/RedisConfig.java b/server/src/main/java/com/ticketing/server/global/redis/RedisConfig.java index 1249dec..188bb7f 100644 --- a/server/src/main/java/com/ticketing/server/global/redis/RedisConfig.java +++ b/server/src/main/java/com/ticketing/server/global/redis/RedisConfig.java @@ -3,10 +3,14 @@ package com.ticketing.server.global.redis; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.transaction.PlatformTransactionManager; @@ -41,4 +45,21 @@ public class RedisConfig { return new JpaTransactionManager(); } + @Bean + public RedisCacheManager redisCacheManager() { + RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new StringRedisSerializer())) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new GenericJackson2JsonRedisSerializer())); + + return RedisCacheManager.RedisCacheManagerBuilder + .fromConnectionFactory(redisConnectionFactory()) + .cacheDefaults(redisCacheConfiguration) + .build(); + + } + } diff --git a/server/src/main/java/com/ticketing/server/global/security/WebSecurityConfig.java b/server/src/main/java/com/ticketing/server/global/security/WebSecurityConfig.java index b42cfa0..37bc621 100644 --- a/server/src/main/java/com/ticketing/server/global/security/WebSecurityConfig.java +++ b/server/src/main/java/com/ticketing/server/global/security/WebSecurityConfig.java @@ -53,8 +53,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { .antMatchers(HttpMethod.POST, "/api/auth/token").permitAll() .antMatchers(HttpMethod.POST, "/api/auth/refresh").permitAll() .antMatchers(HttpMethod.POST, "/api/users").permitAll() - .antMatchers(HttpMethod.GET,"/api/movies").permitAll() - .antMatchers("/api/movieTimes/**").permitAll() + .antMatchers(HttpMethod.GET, "/api/movies").permitAll() .antMatchers("/l7check").permitAll() .antMatchers("/actuator/**").permitAll() .antMatchers("/api/v3/", "/swagger-ui/**", "/swagger/", "/swagger-resources/**", "/v3/api-docs").permitAll() diff --git a/server/src/main/java/com/ticketing/server/global/validator/constraints/NotEmptyCollection.java b/server/src/main/java/com/ticketing/server/global/validator/constraints/NotEmptyCollection.java new file mode 100644 index 0000000..0524ca8 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/global/validator/constraints/NotEmptyCollection.java @@ -0,0 +1,22 @@ +package com.ticketing.server.global.validator.constraints; + +import com.ticketing.server.global.validator.constraintvalidators.NotEmptyCollectionValidator; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import javax.validation.Constraint; +import javax.validation.Payload; + +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = NotEmptyCollectionValidator.class) +public @interface NotEmptyCollection { + + String message() default "목록이 존재하지 않습니다."; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/server/src/main/java/com/ticketing/server/global/validator/constraintvalidators/NotEmptyCollectionValidator.java b/server/src/main/java/com/ticketing/server/global/validator/constraintvalidators/NotEmptyCollectionValidator.java new file mode 100644 index 0000000..38c890e --- /dev/null +++ b/server/src/main/java/com/ticketing/server/global/validator/constraintvalidators/NotEmptyCollectionValidator.java @@ -0,0 +1,20 @@ +package com.ticketing.server.global.validator.constraintvalidators; + +import com.ticketing.server.global.validator.constraints.NotEmptyCollection; +import java.util.Collection; +import java.util.Objects; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class NotEmptyCollectionValidator implements ConstraintValidator> { + + @Override + public boolean isValid(Collection objects, ConstraintValidatorContext context) { + if (objects.isEmpty()) { + return false; + } + + return objects.stream().allMatch(Objects::nonNull); + } + +} diff --git a/server/src/main/java/com/ticketing/server/movie/application/TicketController.java b/server/src/main/java/com/ticketing/server/movie/application/TicketController.java index 5091418..a14c67e 100644 --- a/server/src/main/java/com/ticketing/server/movie/application/TicketController.java +++ b/server/src/main/java/com/ticketing/server/movie/application/TicketController.java @@ -21,10 +21,12 @@ public class TicketController { private final TicketService ticketService; - @GetMapping("payments/{paymentId}") + @GetMapping("/payments/{paymentId}") public ResponseEntity findTicketsByPaymentId(@PathVariable("paymentId") @NotNull Long paymentId) { TicketDetailsDTO tickets = ticketService.findTicketsByPaymentId(paymentId); - return ResponseEntity.status(HttpStatus.OK).body(tickets.toResponse()); + + return ResponseEntity.status(HttpStatus.OK) + .body(tickets.toResponse()); } } diff --git a/server/src/main/java/com/ticketing/server/movie/application/request/TicketCancelRequest.java b/server/src/main/java/com/ticketing/server/movie/application/request/TicketCancelRequest.java new file mode 100644 index 0000000..3f73f20 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/movie/application/request/TicketCancelRequest.java @@ -0,0 +1,13 @@ +package com.ticketing.server.movie.application.request; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TicketCancelRequest { + + private final List ticketIds; + +} diff --git a/server/src/main/java/com/ticketing/server/movie/application/request/TicketReservationRequest.java b/server/src/main/java/com/ticketing/server/movie/application/request/TicketReservationRequest.java new file mode 100644 index 0000000..026c66d --- /dev/null +++ b/server/src/main/java/com/ticketing/server/movie/application/request/TicketReservationRequest.java @@ -0,0 +1,15 @@ +package com.ticketing.server.movie.application.request; + +import com.ticketing.server.global.validator.constraints.NotEmptyCollection; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TicketReservationRequest { + + @NotEmptyCollection + private List ticketIds; + +} diff --git a/server/src/main/java/com/ticketing/server/movie/application/request/TicketSoldRequest.java b/server/src/main/java/com/ticketing/server/movie/application/request/TicketSoldRequest.java new file mode 100644 index 0000000..8f8e7ed --- /dev/null +++ b/server/src/main/java/com/ticketing/server/movie/application/request/TicketSoldRequest.java @@ -0,0 +1,19 @@ +package com.ticketing.server.movie.application.request; + +import com.ticketing.server.global.validator.constraints.NotEmptyCollection; +import java.util.List; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TicketSoldRequest { + + @NotNull + private Long paymentId; + + @NotEmptyCollection + private List ticketIds; + +} diff --git a/server/src/main/java/com/ticketing/server/movie/application/response/TicketCancelResponse.java b/server/src/main/java/com/ticketing/server/movie/application/response/TicketCancelResponse.java new file mode 100644 index 0000000..42ab04a --- /dev/null +++ b/server/src/main/java/com/ticketing/server/movie/application/response/TicketCancelResponse.java @@ -0,0 +1,13 @@ +package com.ticketing.server.movie.application.response; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TicketCancelResponse { + + private final List ticketIds; + +} diff --git a/server/src/main/java/com/ticketing/server/movie/application/response/TicketReservationResponse.java b/server/src/main/java/com/ticketing/server/movie/application/response/TicketReservationResponse.java new file mode 100644 index 0000000..c48d1bd --- /dev/null +++ b/server/src/main/java/com/ticketing/server/movie/application/response/TicketReservationResponse.java @@ -0,0 +1,25 @@ +package com.ticketing.server.movie.application.response; + +import com.ticketing.server.movie.service.dto.TicketReservationDTO; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TicketReservationResponse { + + private final String movieTitle; + private final List tickets; + + public int getTicketQuantity() { + return tickets.size(); + } + + public int getTotalPrice() { + return tickets.stream() + .mapToInt(TicketReservationDTO::getTicketPrice) + .sum(); + } + +} diff --git a/server/src/main/java/com/ticketing/server/movie/application/response/TicketSoldResponse.java b/server/src/main/java/com/ticketing/server/movie/application/response/TicketSoldResponse.java new file mode 100644 index 0000000..838bca9 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/movie/application/response/TicketSoldResponse.java @@ -0,0 +1,15 @@ +package com.ticketing.server.movie.application.response; + +import com.ticketing.server.movie.service.dto.TicketSoldDTO; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TicketSoldResponse { + + private final Long paymentId; + private final List soldDtoList; + +} diff --git a/server/src/main/java/com/ticketing/server/movie/domain/Ticket.java b/server/src/main/java/com/ticketing/server/movie/domain/Ticket.java index 94c72e6..71a7284 100644 --- a/server/src/main/java/com/ticketing/server/movie/domain/Ticket.java +++ b/server/src/main/java/com/ticketing/server/movie/domain/Ticket.java @@ -53,6 +53,38 @@ public class Ticket extends AbstractEntity { this.status = TicketStatus.SALE; } + public Ticket makeReservation() { + if (!TicketStatus.SALE.equals(status)) { + throw ErrorCode.throwDuplicatePayment(); + } + + status = TicketStatus.RESERVATION; + return this; + } + + public Ticket makeSold(Long paymentId) { + if (TicketStatus.SOLD.equals(status)) { + throw ErrorCode.throwDuplicatePayment(); + } + + status = TicketStatus.SOLD; + this.paymentId = paymentId; + return this; + } + + public Ticket cancel() { + if (!TicketStatus.RESERVATION.equals(status)) { + throw ErrorCode.throwBadRequestPaymentCancel(); + } + + status = TicketStatus.SALE; + return this; + } + + public Long getMovieTimeId() { + return movieTime.getId(); + } + public Integer getColumn() { return this.seat.getSeatColumn(); } @@ -85,5 +117,4 @@ public class Ticket extends AbstractEntity { this.paymentId = id; } - } diff --git a/server/src/main/java/com/ticketing/server/movie/domain/TicketStatus.java b/server/src/main/java/com/ticketing/server/movie/domain/TicketStatus.java index c4dd331..14f2420 100644 --- a/server/src/main/java/com/ticketing/server/movie/domain/TicketStatus.java +++ b/server/src/main/java/com/ticketing/server/movie/domain/TicketStatus.java @@ -2,15 +2,16 @@ package com.ticketing.server.movie.domain; import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.RequiredArgsConstructor; @Getter -@AllArgsConstructor +@RequiredArgsConstructor public enum TicketStatus { SALE("판매가능"), - SCHEDULED("환불"), + RESERVATION("예약"), SOLD("판매완료"); - private String name; + private final String name; } diff --git a/server/src/main/java/com/ticketing/server/movie/domain/repository/TicketRepository.java b/server/src/main/java/com/ticketing/server/movie/domain/repository/TicketRepository.java index 4298b6f..15694dc 100644 --- a/server/src/main/java/com/ticketing/server/movie/domain/repository/TicketRepository.java +++ b/server/src/main/java/com/ticketing/server/movie/domain/repository/TicketRepository.java @@ -21,4 +21,15 @@ public interface TicketRepository extends JpaRepository { ) List findTicketFetchJoinByPaymentId(@Param("paymentId") Long paymentId); + @Query( + value = + "SELECT t " + + "FROM Ticket t " + + "JOIN FETCH t.movieTime mt " + + "JOIN FETCH t.seat s " + + "JOIN FETCH s.theater th " + + "WHERE t.id IN (:ticketIds) " + ) + List findTicketFetchJoinByTicketIds(@Param("ticketIds") List ticketIds); + } diff --git a/server/src/main/java/com/ticketing/server/movie/service/TicketServiceImpl.java b/server/src/main/java/com/ticketing/server/movie/service/TicketServiceImpl.java index 4a33370..ed0dd01 100644 --- a/server/src/main/java/com/ticketing/server/movie/service/TicketServiceImpl.java +++ b/server/src/main/java/com/ticketing/server/movie/service/TicketServiceImpl.java @@ -1,8 +1,15 @@ package com.ticketing.server.movie.service; import com.ticketing.server.global.exception.ErrorCode; +import com.ticketing.server.global.validator.constraints.NotEmptyCollection; +import com.ticketing.server.movie.domain.Ticket; import com.ticketing.server.movie.domain.repository.TicketRepository; import com.ticketing.server.movie.service.dto.TicketDetailsDTO; +import com.ticketing.server.movie.service.dto.TicketReservationDTO; +import com.ticketing.server.movie.service.dto.TicketSoldDTO; +import com.ticketing.server.movie.service.dto.TicketsCancelDTO; +import com.ticketing.server.movie.service.dto.TicketsReservationDTO; +import com.ticketing.server.movie.service.dto.TicketsSoldDTO; import com.ticketing.server.movie.service.interfaces.TicketService; import com.ticketing.server.payment.service.dto.TicketDetailDTO; import java.util.List; @@ -37,4 +44,65 @@ public class TicketServiceImpl implements TicketService { return new TicketDetailsDTO(ticketDetails); } + @Override + @Transactional + public TicketsReservationDTO ticketReservation(@NotEmptyCollection List ticketIds) { + List tickets = getTicketsByInTicketIds(ticketIds); + + Long firstMovieTimeId = firstMovieTimeId(tickets); + List reservationDtoList = tickets.stream() + .map(Ticket::makeReservation) + .filter(ticket -> firstMovieTimeId.equals(ticket.getMovieTimeId())) + .map(TicketReservationDTO::new) + .collect(Collectors.toList()); + + if (ticketIds.size() != reservationDtoList.size()) { + throw ErrorCode.throwBadRequestMovieTime(); + } + + return new TicketsReservationDTO(firstMovieTitle(tickets), reservationDtoList); + } + + @Override + @Transactional + public TicketsSoldDTO ticketSold(@NotNull Long paymentId, @NotEmptyCollection List ticketIds) { + List tickets = getTicketsByInTicketIds(ticketIds); + + List soldDtoList = tickets.stream() + .map(ticket -> ticket.makeSold(paymentId)) + .map(TicketSoldDTO::new) + .collect(Collectors.toList()); + + return new TicketsSoldDTO(paymentId, soldDtoList); + } + + @Override + @Transactional + public TicketsCancelDTO ticketCancel(@NotEmptyCollection List ticketIds) { + List tickets = getTicketsByInTicketIds(ticketIds); + tickets.forEach(Ticket::cancel); + + return new TicketsCancelDTO(ticketIds); + } + + private List getTicketsByInTicketIds(List ticketIds) { + List tickets = ticketRepository.findTicketFetchJoinByTicketIds(ticketIds); + + if (tickets.size() != ticketIds.size()) { + throw ErrorCode.throwInvalidTicketId(); + } + + return tickets; + } + + private Long firstMovieTimeId(List tickets) { + Ticket ticket = tickets.get(0); + return ticket.getMovieTimeId(); + } + + private String firstMovieTitle(List tickets) { + Ticket ticket = tickets.get(0); + return ticket.getMovieTitle(); + } + } diff --git a/server/src/main/java/com/ticketing/server/movie/service/dto/TicketReservationDTO.java b/server/src/main/java/com/ticketing/server/movie/service/dto/TicketReservationDTO.java new file mode 100644 index 0000000..e593384 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/movie/service/dto/TicketReservationDTO.java @@ -0,0 +1,19 @@ +package com.ticketing.server.movie.service.dto; + +import com.ticketing.server.movie.domain.Ticket; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class TicketReservationDTO { + + private final Long ticketId; + private final Integer ticketPrice; + + public TicketReservationDTO(Ticket ticket) { + this(ticket.getId(), ticket.getTicketPrice()); + } + +} diff --git a/server/src/main/java/com/ticketing/server/movie/service/dto/TicketSoldDTO.java b/server/src/main/java/com/ticketing/server/movie/service/dto/TicketSoldDTO.java new file mode 100644 index 0000000..79ece28 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/movie/service/dto/TicketSoldDTO.java @@ -0,0 +1,19 @@ +package com.ticketing.server.movie.service.dto; + +import com.ticketing.server.movie.domain.Ticket; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class TicketSoldDTO { + + private final Long ticketId; + private final Integer ticketPrice; + + public TicketSoldDTO(Ticket ticket) { + this(ticket.getId(), ticket.getTicketPrice()); + } + +} diff --git a/server/src/main/java/com/ticketing/server/movie/service/dto/TicketsCancelDTO.java b/server/src/main/java/com/ticketing/server/movie/service/dto/TicketsCancelDTO.java new file mode 100644 index 0000000..aea8df8 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/movie/service/dto/TicketsCancelDTO.java @@ -0,0 +1,18 @@ +package com.ticketing.server.movie.service.dto; + +import com.ticketing.server.movie.application.response.TicketCancelResponse; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TicketsCancelDTO { + + private final List ticketIds; + + public TicketCancelResponse toResponse() { + return new TicketCancelResponse(ticketIds); + } + +} diff --git a/server/src/main/java/com/ticketing/server/movie/service/dto/TicketsReservationDTO.java b/server/src/main/java/com/ticketing/server/movie/service/dto/TicketsReservationDTO.java new file mode 100644 index 0000000..af60879 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/movie/service/dto/TicketsReservationDTO.java @@ -0,0 +1,19 @@ +package com.ticketing.server.movie.service.dto; + +import com.ticketing.server.movie.application.response.TicketReservationResponse; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TicketsReservationDTO { + + private final String movieTitle; + private final List ticketReservationDtoList; + + public TicketReservationResponse toResponse() { + return new TicketReservationResponse(movieTitle, ticketReservationDtoList); + } + +} diff --git a/server/src/main/java/com/ticketing/server/movie/service/dto/TicketsSoldDTO.java b/server/src/main/java/com/ticketing/server/movie/service/dto/TicketsSoldDTO.java new file mode 100644 index 0000000..ff52bd0 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/movie/service/dto/TicketsSoldDTO.java @@ -0,0 +1,19 @@ +package com.ticketing.server.movie.service.dto; + +import com.ticketing.server.movie.application.response.TicketSoldResponse; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TicketsSoldDTO { + + private final Long paymentId; + private final List soldDtoList; + + public TicketSoldResponse toResponse() { + return new TicketSoldResponse(paymentId, soldDtoList); + } + +} diff --git a/server/src/main/java/com/ticketing/server/movie/service/interfaces/TicketService.java b/server/src/main/java/com/ticketing/server/movie/service/interfaces/TicketService.java index 46a3151..61b6e54 100644 --- a/server/src/main/java/com/ticketing/server/movie/service/interfaces/TicketService.java +++ b/server/src/main/java/com/ticketing/server/movie/service/interfaces/TicketService.java @@ -1,10 +1,21 @@ package com.ticketing.server.movie.service.interfaces; +import com.ticketing.server.global.validator.constraints.NotEmptyCollection; import com.ticketing.server.movie.service.dto.TicketDetailsDTO; +import com.ticketing.server.movie.service.dto.TicketsCancelDTO; +import com.ticketing.server.movie.service.dto.TicketsReservationDTO; +import com.ticketing.server.movie.service.dto.TicketsSoldDTO; +import java.util.List; import javax.validation.constraints.NotNull; public interface TicketService { TicketDetailsDTO findTicketsByPaymentId(@NotNull Long paymentId); + TicketsReservationDTO ticketReservation(@NotEmptyCollection List ticketIds); + + TicketsSoldDTO ticketSold(@NotNull Long paymentId, @NotEmptyCollection List ticketIds); + + TicketsCancelDTO ticketCancel(@NotEmptyCollection List ticketIds); + } diff --git a/server/src/main/java/com/ticketing/server/movie/setup/MovieSetupService.java b/server/src/main/java/com/ticketing/server/movie/setup/MovieSetupService.java index 8fde7b4..f6b1289 100644 --- a/server/src/main/java/com/ticketing/server/movie/setup/MovieSetupService.java +++ b/server/src/main/java/com/ticketing/server/movie/setup/MovieSetupService.java @@ -1,5 +1,6 @@ package com.ticketing.server.movie.setup; +import com.ticketing.server.global.redis.PaymentCache; import com.ticketing.server.movie.domain.Movie; import com.ticketing.server.movie.domain.MovieTime; import com.ticketing.server.movie.domain.Seat; @@ -13,7 +14,6 @@ import com.ticketing.server.payment.domain.Payment; import com.ticketing.server.payment.domain.PaymentStatus; import com.ticketing.server.payment.domain.PaymentType; import com.ticketing.server.payment.domain.repository.PaymentRepository; -import com.ticketing.server.payment.service.dto.CreatePaymentDTO; import com.ticketing.server.user.domain.User; import com.ticketing.server.user.domain.UserGrade; import com.ticketing.server.user.domain.repository.UserRepository; @@ -119,8 +119,16 @@ MovieSetupService { List tickets = ticketRepository.findAll(); Ticket ticket = tickets.get(0); String title = ticket.getMovieTime().getMovie().getTitle(); - CreatePaymentDTO createPaymentDto = new CreatePaymentDTO(user.getAlternateId(), title, PaymentType.KAKAO_PAY, PaymentStatus.COMPLETED, "2022-0710-4142", 30_000); - Payment payment = createPaymentDto.toEntity(); + PaymentCache paymentCache = new PaymentCache( + user.getEmail(), + title, + "T2d03c9130bf237a9700", + List.of(1L, 2L), + user.getAlternateId(), + 124124231513245L + ); + + Payment payment = new Payment(paymentCache, PaymentType.KAKAO_PAY, PaymentStatus.SOLD, 30_000); paymentRepository.save(payment); diff --git a/server/src/main/java/com/ticketing/server/payment/api/CoreFeignConfiguration.java b/server/src/main/java/com/ticketing/server/payment/api/CoreFeignConfiguration.java new file mode 100644 index 0000000..ef28009 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/api/CoreFeignConfiguration.java @@ -0,0 +1,20 @@ +package com.ticketing.server.payment.api; + +import feign.Logger; +import feign.Logger.Level; +import feign.codec.Encoder; +import org.springframework.context.annotation.Bean; + +public class CoreFeignConfiguration { + + @Bean + Logger.Level feignLoggerLevel() { + return Level.FULL; + } + + @Bean + Encoder formEncoder() { + return new feign.form.FormEncoder(); + } + +} diff --git a/server/src/main/java/com/ticketing/server/payment/api/KakaoPayClient.java b/server/src/main/java/com/ticketing/server/payment/api/KakaoPayClient.java new file mode 100644 index 0000000..8371740 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/api/KakaoPayClient.java @@ -0,0 +1,26 @@ +package com.ticketing.server.payment.api; + +import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE; + +import com.ticketing.server.payment.api.dto.requset.KakaoPayApproveRequest; +import com.ticketing.server.payment.api.dto.requset.KakaoPayReadyRequest; +import com.ticketing.server.payment.api.dto.response.KakaoPayApproveResponse; +import com.ticketing.server.payment.api.dto.response.KakaoPayReadyResponse; +import feign.Headers; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; + +@FeignClient(name = "kakaoPay", url = "https://kapi.kakao.com/v1", path = "/payment", + configuration = CoreFeignConfiguration.class) +public interface KakaoPayClient { + + @PostMapping(value = "/ready", consumes = APPLICATION_FORM_URLENCODED_VALUE) + @Headers("Content-Type: application/x-www-form-urlencoded;charset=utf-8") + KakaoPayReadyResponse ready(@RequestHeader(value = "Authorization") String authorization, KakaoPayReadyRequest request); + + @PostMapping(value = "/approve", consumes = APPLICATION_FORM_URLENCODED_VALUE) + @Headers("Content-Type: application/x-www-form-urlencoded;charset=utf-8") + KakaoPayApproveResponse approve(@RequestHeader(value = "Authorization") String authorization, KakaoPayApproveRequest request); + +} diff --git a/server/src/main/java/com/ticketing/server/payment/api/KakaoPayProperties.java b/server/src/main/java/com/ticketing/server/payment/api/KakaoPayProperties.java new file mode 100644 index 0000000..19fcf0c --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/api/KakaoPayProperties.java @@ -0,0 +1,40 @@ +package com.ticketing.server.payment.api; + +import com.ticketing.server.global.factory.YamlPropertySourceFactory; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.context.annotation.PropertySource; + +@Getter +@RequiredArgsConstructor +@ConstructorBinding +@ConfigurationProperties(value = "api.kakao.pay") +@PropertySource(value = "classpath:application.yml", factory = YamlPropertySourceFactory.class) +public class KakaoPayProperties { + + private final String url; + private final String approval; + private final String cancel; + private final String fail; + private final String prefix; + private final String key; + + public String getApprovalUrl() { + return url + approval; + } + + public String getCancelUrl() { + return url + cancel; + } + + public String getFailUrl() { + return url + fail; + } + + public String getAuthorization() { + return prefix + " " + key; + } + +} diff --git a/server/src/main/java/com/ticketing/server/payment/api/MovieClient.java b/server/src/main/java/com/ticketing/server/payment/api/MovieClient.java index a76643b..55fd333 100644 --- a/server/src/main/java/com/ticketing/server/payment/api/MovieClient.java +++ b/server/src/main/java/com/ticketing/server/payment/api/MovieClient.java @@ -1,10 +1,21 @@ package com.ticketing.server.payment.api; +import com.ticketing.server.movie.application.request.TicketCancelRequest; +import com.ticketing.server.movie.application.request.TicketReservationRequest; +import com.ticketing.server.movie.application.response.TicketCancelResponse; import com.ticketing.server.movie.application.response.TicketDetailsResponse; +import com.ticketing.server.movie.application.response.TicketReservationResponse; +import com.ticketing.server.movie.application.request.TicketSoldRequest; +import com.ticketing.server.movie.application.response.TicketSoldResponse; import javax.validation.constraints.NotNull; public interface MovieClient { TicketDetailsResponse getTicketsByPaymentId(@NotNull Long paymentId); + TicketReservationResponse ticketReservation(@NotNull TicketReservationRequest request); + + TicketSoldResponse ticketSold(@NotNull TicketSoldRequest request); + + TicketCancelResponse ticketCancel(@NotNull TicketCancelRequest request); } diff --git a/server/src/main/java/com/ticketing/server/payment/api/dto/SnakeCaseStrategy.java b/server/src/main/java/com/ticketing/server/payment/api/dto/SnakeCaseStrategy.java new file mode 100644 index 0000000..c740db8 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/api/dto/SnakeCaseStrategy.java @@ -0,0 +1,12 @@ +package com.ticketing.server.payment.api.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) +@JsonInclude(Include.NON_NULL) +public class SnakeCaseStrategy { + +} diff --git a/server/src/main/java/com/ticketing/server/payment/api/dto/requset/KakaoPayApproveRequest.java b/server/src/main/java/com/ticketing/server/payment/api/dto/requset/KakaoPayApproveRequest.java new file mode 100644 index 0000000..b431b5d --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/api/dto/requset/KakaoPayApproveRequest.java @@ -0,0 +1,50 @@ +package com.ticketing.server.payment.api.dto.requset; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import javax.validation.constraints.NotEmpty; +import lombok.Getter; + +@Getter +@JsonInclude(Include.NON_NULL) +public class KakaoPayApproveRequest { + + // 가맹점 코드 현재는 테스트 값 + @NotEmpty + private String cid = "TC0ONETIME"; + + @feign.form.FormProperty("cid_secret") + private String cidSecret; + + // 결제 고유 번호 ready res 에 포함 + @NotEmpty + private String tid; + + // 주문번호 - 결제 예매번호? + @NotEmpty + @feign.form.FormProperty("partner_order_id") + private String partnerOrderId; + + // 가맹점 회원 id - user 대체 ID + @NotEmpty + @feign.form.FormProperty("partner_user_id") + private String partnerUserId; + + // 결제승인 요청 인증 토큰 + @NotEmpty + @feign.form.FormProperty("pg_token") + private String pgToken; + + private String payload; + + @feign.form.FormProperty("total_amount") + private Integer totalAmount; + + public KakaoPayApproveRequest(String tid, String partnerOrderId, String partnerUserId, String pgToken) { + this.tid = tid; + this.partnerOrderId = partnerOrderId; + this.partnerUserId = partnerUserId; + this.pgToken = pgToken; + } + +} diff --git a/server/src/main/java/com/ticketing/server/payment/api/dto/requset/KakaoPayReadyRequest.java b/server/src/main/java/com/ticketing/server/payment/api/dto/requset/KakaoPayReadyRequest.java new file mode 100644 index 0000000..0dfde12 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/api/dto/requset/KakaoPayReadyRequest.java @@ -0,0 +1,92 @@ +package com.ticketing.server.payment.api.dto.requset; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(Include.NON_NULL) +public class KakaoPayReadyRequest { + + // 가맹점 코드 + @NotEmpty + private String cid = "TC0ONETIME"; + + // 가맹점 코드 인증키 + @feign.form.FormProperty("cid_secret") + private String cidSecret; + + // 가맹점 주문번호 - 결제 예매번호? + @NotEmpty + @feign.form.FormProperty("partner_order_id") + private String partnerOrderId; + + // 가맹점 회원 id - user 대체 ID + @NotEmpty + @feign.form.FormProperty("partner_user_id") + private String partnerUserId; + + // 상품명 - 영화제목 + @NotEmpty + @feign.form.FormProperty("item_name") + private String itemName; + + // 상품코드 + @feign.form.FormProperty("item_code") + private String itemCode; + + // 상품수량 - 티켓 수량 + @NotNull + private Integer quantity; + + // 상품 총액 - 티켓 금액 합계 + @NotNull + @feign.form.FormProperty("total_amount") + private Integer totalAmount; + + // 상품 비과세 - 0원으로 일단 일괄처리 + @NotNull + @feign.form.FormProperty("tax_free_amount") + private Integer taxFreeAmount = 0; + + // 상품 부가세 금액 + @feign.form.FormProperty("vat_amount") + private Integer vatAmount; + + // 컵 보증금 + @feign.form.FormProperty("green_deposit") + private Integer greenDeposit; + + // 결제 성공 시 redirect URL - json 반환 용 URL ? + @NotNull + @feign.form.FormProperty("approval_url") + private String approvalUrl; + + // 결제 취소 시 redirect URL + @NotNull + @feign.form.FormProperty("cancel_url") + private String cancelUrl; + + // 결제 실패 시 redirect URL + @NotNull + @feign.form.FormProperty("fail_url") + private String failUrl; + + public KakaoPayReadyRequest(String partnerOrderId, String partnerUserId, String itemName, Integer quantity, + Integer totalAmount, String approvalUrl, String cancelUrl, String failUrl) { + this.partnerOrderId = partnerOrderId; + this.partnerUserId = partnerUserId; + this.itemName = itemName; + this.quantity = quantity; + this.totalAmount = totalAmount; + this.approvalUrl = approvalUrl; + this.cancelUrl = cancelUrl; + this.failUrl = failUrl; + } +} diff --git a/server/src/main/java/com/ticketing/server/payment/api/dto/response/Amount.java b/server/src/main/java/com/ticketing/server/payment/api/dto/response/Amount.java new file mode 100644 index 0000000..152e55a --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/api/dto/response/Amount.java @@ -0,0 +1,27 @@ +package com.ticketing.server.payment.api.dto.response; + +import com.ticketing.server.payment.api.dto.SnakeCaseStrategy; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class Amount extends SnakeCaseStrategy { + + private Integer total; + + private Integer taxFree; + + private Integer vat; + + private Integer point; + + private Integer discount; + + private Integer greenDeposit; + +} diff --git a/server/src/main/java/com/ticketing/server/payment/api/dto/response/CardInfo.java b/server/src/main/java/com/ticketing/server/payment/api/dto/response/CardInfo.java new file mode 100644 index 0000000..a4222ea --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/api/dto/response/CardInfo.java @@ -0,0 +1,45 @@ +package com.ticketing.server.payment.api.dto.response; + +import com.ticketing.server.payment.api.dto.SnakeCaseStrategy; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class CardInfo extends SnakeCaseStrategy { + + private String purchaseCorp; + + private String purchaseCorpCode; + + private String issuerCorp; + + private String issuerCorpCode; + + private String kakaopayPurchaseCorp; + + private String kakaopayPurchaseCorpCode; + + private String kakaopayIssuerCorp; + + private String kakaopayIssuerCorpCode; + + private String bin; + + private String cardType; + + private String installMonth; + + private String approvedId; + + private String cardMid; + + private String interestFreeInstall; + + private String cardItemCode; + +} diff --git a/server/src/main/java/com/ticketing/server/payment/api/dto/response/KakaoPayApproveResponse.java b/server/src/main/java/com/ticketing/server/payment/api/dto/response/KakaoPayApproveResponse.java new file mode 100644 index 0000000..6769de6 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/api/dto/response/KakaoPayApproveResponse.java @@ -0,0 +1,51 @@ +package com.ticketing.server.payment.api.dto.response; + + +import com.ticketing.server.payment.api.dto.SnakeCaseStrategy; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class KakaoPayApproveResponse extends SnakeCaseStrategy { + + private String aid; + + private String tid; + + private String cid; + + private String sid; + + private String partnerOrderId; + + private String partnerUserId; + + private String paymentMethodType; + + private Amount amount; + + private CardInfo cardInfo; + + private String itemName; + + private String itemCode; + + private Integer quantity; + + private LocalDateTime createdAt; + + private LocalDateTime approvedAt; + + private String payload; + + public Integer getTotalAmount() { + return amount.getTotal(); + } + +} diff --git a/server/src/main/java/com/ticketing/server/payment/api/dto/response/KakaoPayReadyResponse.java b/server/src/main/java/com/ticketing/server/payment/api/dto/response/KakaoPayReadyResponse.java new file mode 100644 index 0000000..2a4c228 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/api/dto/response/KakaoPayReadyResponse.java @@ -0,0 +1,37 @@ +package com.ticketing.server.payment.api.dto.response; + +import com.ticketing.server.payment.api.dto.SnakeCaseStrategy; +import java.time.LocalDateTime; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class KakaoPayReadyResponse extends SnakeCaseStrategy { + + // 결제 고유 번호 + private String tid; + + // 요청 클라이언트가 모바일 앱일 경우 + private String nextRedirectAppUrl; + + // 요청 클라이언트가 모바일 웹일 경우 + private String nextRedirectMobileUrl; + + // 요청 클라이언트가 PC 웹일 경우 + private String nextRedirectPcUrl; + + // 카카오페이 결제 화면으로 이동하는 Android 앱 스킴 + private String androidAppScheme; + + // 카카오페이 결제 화면으로 이동하는 IOS 앱 스킴 + private String iosAppScheme; + + // 결제 준비 요청 시간 + private LocalDateTime createAt; + +} diff --git a/server/src/main/java/com/ticketing/server/payment/api/impl/MovieClientImpl.java b/server/src/main/java/com/ticketing/server/payment/api/impl/MovieClientImpl.java index 025ab72..e598b4c 100644 --- a/server/src/main/java/com/ticketing/server/payment/api/impl/MovieClientImpl.java +++ b/server/src/main/java/com/ticketing/server/payment/api/impl/MovieClientImpl.java @@ -1,7 +1,16 @@ package com.ticketing.server.payment.api.impl; +import com.ticketing.server.movie.application.request.TicketCancelRequest; +import com.ticketing.server.movie.application.request.TicketReservationRequest; +import com.ticketing.server.movie.application.request.TicketSoldRequest; +import com.ticketing.server.movie.application.response.TicketCancelResponse; import com.ticketing.server.movie.application.response.TicketDetailsResponse; +import com.ticketing.server.movie.application.response.TicketReservationResponse; +import com.ticketing.server.movie.application.response.TicketSoldResponse; import com.ticketing.server.movie.service.dto.TicketDetailsDTO; +import com.ticketing.server.movie.service.dto.TicketsCancelDTO; +import com.ticketing.server.movie.service.dto.TicketsReservationDTO; +import com.ticketing.server.movie.service.dto.TicketsSoldDTO; import com.ticketing.server.movie.service.interfaces.TicketService; import com.ticketing.server.payment.api.MovieClient; import javax.validation.constraints.NotNull; @@ -22,4 +31,22 @@ public class MovieClientImpl implements MovieClient { return ticketDetails.toResponse(); } + @Override + public TicketReservationResponse ticketReservation(@NotNull TicketReservationRequest request) { + TicketsReservationDTO ticketReservationsDto = ticketService.ticketReservation(request.getTicketIds()); + return ticketReservationsDto.toResponse(); + } + + @Override + public TicketSoldResponse ticketSold(@NotNull TicketSoldRequest request) { + TicketsSoldDTO ticketsSoldDto = ticketService.ticketSold(request.getPaymentId(), request.getTicketIds()); + return ticketsSoldDto.toResponse(); + } + + @Override + public TicketCancelResponse ticketCancel(@NotNull TicketCancelRequest request) { + TicketsCancelDTO ticketsCancelDto = ticketService.ticketCancel(request.getTicketIds()); + return ticketsCancelDto.toResponse(); + } + } diff --git a/server/src/main/java/com/ticketing/server/payment/application/PaymentController.java b/server/src/main/java/com/ticketing/server/payment/application/PaymentController.java index 6f5485e..820c71f 100644 --- a/server/src/main/java/com/ticketing/server/payment/application/PaymentController.java +++ b/server/src/main/java/com/ticketing/server/payment/application/PaymentController.java @@ -1,20 +1,32 @@ package com.ticketing.server.payment.application; +import com.ticketing.server.payment.application.request.PaymentReadyRequest; import com.ticketing.server.payment.application.response.PaymentDetailResponse; import com.ticketing.server.payment.application.response.SimplePaymentsResponse; +import com.ticketing.server.payment.service.dto.PaymentCancelDTO; +import com.ticketing.server.payment.application.response.PaymentCancelResponse; +import com.ticketing.server.payment.service.dto.PaymentCompleteDTO; +import com.ticketing.server.payment.application.response.PaymentCompleteResponse; import com.ticketing.server.payment.service.dto.PaymentDetailDTO; +import com.ticketing.server.payment.service.dto.PaymentReadyDTO; import com.ticketing.server.payment.service.dto.SimplePaymentsDTO; import com.ticketing.server.payment.service.interfaces.PaymentApisService; import com.ticketing.server.payment.service.interfaces.PaymentService; import com.ticketing.server.user.domain.UserGrade; +import javax.validation.Valid; import javax.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -44,4 +56,33 @@ public class PaymentController { .body(paymentDetail.toResponse()); } + @PostMapping("/ready") + @Secured(UserGrade.ROLES.USER) + public ResponseEntity ready(@RequestBody @Valid PaymentReadyRequest request) { + PaymentReadyDTO paymentReadyDto = paymentApisService.ready(request.getTicketIds()); + + return ResponseEntity.status(HttpStatus.OK) + .body(paymentReadyDto); + } + + @GetMapping("/complete") + @Secured(UserGrade.ROLES.USER) + public ResponseEntity complete( + @AuthenticationPrincipal UserDetails userRequest, + @RequestParam("pg_token") String pgToken) { + PaymentCompleteDTO paymentCompleteDto = paymentApisService.complete(userRequest.getUsername(), pgToken); + + return ResponseEntity.status(HttpStatus.OK) + .body(paymentCompleteDto.toResponse()); + } + + @GetMapping("/cancel") + @Secured(UserGrade.ROLES.USER) + public ResponseEntity cancel(@AuthenticationPrincipal UserDetails userRequest) { + PaymentCancelDTO paymentCancelDto = paymentApisService.cancel(userRequest.getUsername()); + + return ResponseEntity.status(HttpStatus.OK) + .body(paymentCancelDto.toResponse()); + } + } diff --git a/server/src/main/java/com/ticketing/server/payment/application/request/PaymentReadyRequest.java b/server/src/main/java/com/ticketing/server/payment/application/request/PaymentReadyRequest.java new file mode 100644 index 0000000..b16c78d --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/application/request/PaymentReadyRequest.java @@ -0,0 +1,13 @@ +package com.ticketing.server.payment.application.request; + +import com.ticketing.server.global.validator.constraints.NotEmptyCollection; +import java.util.List; +import lombok.Getter; + +@Getter +public class PaymentReadyRequest { + + @NotEmptyCollection + private List ticketIds; + +} diff --git a/server/src/main/java/com/ticketing/server/payment/application/response/PaymentCancelResponse.java b/server/src/main/java/com/ticketing/server/payment/application/response/PaymentCancelResponse.java new file mode 100644 index 0000000..a7a602f --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/application/response/PaymentCancelResponse.java @@ -0,0 +1,16 @@ +package com.ticketing.server.payment.application.response; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class PaymentCancelResponse { + + private final String email; + private final String movieTitle; + private final String tid; + private final List ticketIds; + +} diff --git a/server/src/main/java/com/ticketing/server/payment/application/response/PaymentCompleteResponse.java b/server/src/main/java/com/ticketing/server/payment/application/response/PaymentCompleteResponse.java new file mode 100644 index 0000000..4e15249 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/application/response/PaymentCompleteResponse.java @@ -0,0 +1,16 @@ +package com.ticketing.server.payment.application.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class PaymentCompleteResponse { + + private final String email; + private final String tid; + private final String movieTitle; + private final Integer quantity; + private final Integer totalAmount; + +} diff --git a/server/src/main/java/com/ticketing/server/payment/domain/Payment.java b/server/src/main/java/com/ticketing/server/payment/domain/Payment.java index 8d5b7e1..85c1f60 100644 --- a/server/src/main/java/com/ticketing/server/payment/domain/Payment.java +++ b/server/src/main/java/com/ticketing/server/payment/domain/Payment.java @@ -1,6 +1,7 @@ package com.ticketing.server.payment.domain; import com.ticketing.server.global.dto.repository.AbstractEntity; +import com.ticketing.server.global.redis.PaymentCache; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.EnumType; @@ -24,6 +25,10 @@ public class Payment extends AbstractEntity { @Column(name = "movie_title", nullable = false) private String movieTitle; + @NotEmpty + @Column(name = "tid", nullable = false) + private String tid; + @NotNull @Column(name = "type", nullable = false) @Enumerated(value = EnumType.STRING) @@ -45,9 +50,22 @@ public class Payment extends AbstractEntity { @Column(name = "total_price", nullable = false) private Integer totalPrice; - public Payment(Long userAlternateId, String movieTitle, PaymentType type, PaymentStatus status, String paymentNumber, Integer totalPrice) { + public Payment(PaymentCache paymentCache, PaymentType type, PaymentStatus status, Integer totalAmount) { + this( + paymentCache.getUserAlternateId(), + paymentCache.getMovieTitle(), + paymentCache.getTid(), + type, + status, + paymentCache.getPaymentNumber().toString(), + totalAmount + ); + } + + private Payment(Long userAlternateId, String movieTitle, String tid, PaymentType type, PaymentStatus status, String paymentNumber, Integer totalPrice) { this.userAlternateId = userAlternateId; this.movieTitle = movieTitle; + this.tid = tid; this.type = type; this.status = status; this.paymentNumber = paymentNumber; diff --git a/server/src/main/java/com/ticketing/server/payment/domain/PaymentStatus.java b/server/src/main/java/com/ticketing/server/payment/domain/PaymentStatus.java index f96e6ae..c877695 100644 --- a/server/src/main/java/com/ticketing/server/payment/domain/PaymentStatus.java +++ b/server/src/main/java/com/ticketing/server/payment/domain/PaymentStatus.java @@ -1,5 +1,15 @@ package com.ticketing.server.payment.domain; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor public enum PaymentStatus { - COMPLETED, REFUNDED, FAILED + + SOLD("판매"), + REFUNDED("환불"); + + private final String statusName; + } diff --git a/server/src/main/java/com/ticketing/server/payment/service/PaymentApisServiceImpl.java b/server/src/main/java/com/ticketing/server/payment/service/PaymentApisServiceImpl.java index fc5241c..3786e21 100644 --- a/server/src/main/java/com/ticketing/server/payment/service/PaymentApisServiceImpl.java +++ b/server/src/main/java/com/ticketing/server/payment/service/PaymentApisServiceImpl.java @@ -1,14 +1,35 @@ package com.ticketing.server.payment.service; +import com.ticketing.server.global.dto.SequenceGenerator; import com.ticketing.server.global.exception.ErrorCode; +import com.ticketing.server.global.redis.PaymentCache; +import com.ticketing.server.global.redis.PaymentCacheRepository; +import com.ticketing.server.global.validator.constraints.NotEmptyCollection; +import com.ticketing.server.movie.application.request.TicketCancelRequest; +import com.ticketing.server.movie.application.request.TicketReservationRequest; +import com.ticketing.server.movie.application.request.TicketSoldRequest; import com.ticketing.server.movie.application.response.TicketDetailsResponse; +import com.ticketing.server.movie.application.response.TicketReservationResponse; +import com.ticketing.server.payment.api.KakaoPayClient; +import com.ticketing.server.payment.api.KakaoPayProperties; import com.ticketing.server.payment.api.MovieClient; import com.ticketing.server.payment.api.UserClient; +import com.ticketing.server.payment.api.dto.requset.KakaoPayApproveRequest; +import com.ticketing.server.payment.api.dto.requset.KakaoPayReadyRequest; +import com.ticketing.server.payment.api.dto.response.KakaoPayApproveResponse; +import com.ticketing.server.payment.api.dto.response.KakaoPayReadyResponse; import com.ticketing.server.payment.api.dto.response.UserDetailResponse; import com.ticketing.server.payment.domain.Payment; +import com.ticketing.server.payment.domain.PaymentStatus; +import com.ticketing.server.payment.domain.PaymentType; import com.ticketing.server.payment.domain.repository.PaymentRepository; +import com.ticketing.server.payment.service.dto.PaymentCancelDTO; +import com.ticketing.server.payment.service.dto.PaymentCompleteDTO; import com.ticketing.server.payment.service.dto.PaymentDetailDTO; +import com.ticketing.server.payment.service.dto.PaymentReadyDTO; import com.ticketing.server.payment.service.interfaces.PaymentApisService; +import java.util.List; +import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -23,9 +44,16 @@ import org.springframework.validation.annotation.Validated; @Slf4j public class PaymentApisServiceImpl implements PaymentApisService { + private final KakaoPayProperties kakaoPayProperties; + private final PaymentRepository paymentRepository; + private final PaymentCacheRepository paymentCacheRepository; + private final MovieClient movieClient; private final UserClient userClient; + private final KakaoPayClient kakaoPayClient; + + private final SequenceGenerator sequenceGenerator; @Override public PaymentDetailDTO findPaymentDetail(@NotNull Long paymentId) { @@ -41,4 +69,88 @@ public class PaymentApisServiceImpl implements PaymentApisService { return new PaymentDetailDTO(payment, tickets); } + @Override + @Transactional + public PaymentReadyDTO ready(@NotEmptyCollection List ticketIds) { + // User 정보 조회 - user 의 대체 키가 필요함. + UserDetailResponse userResponse = userClient.detail(); + Long userAlternateId = userResponse.getUserAlternateId(); + + // Ticket 정보 조회 - 영화제목, 티켓가격이 필요함. + TicketReservationResponse ticketResponse = movieClient.ticketReservation(new TicketReservationRequest(ticketIds)); + String movieTitle = ticketResponse.getMovieTitle(); + + // 예매번호 자동생성 + Long paymentNumber = sequenceGenerator.generateId(); + + KakaoPayReadyRequest request = new KakaoPayReadyRequest( + paymentNumber.toString(), + userAlternateId.toString(), + movieTitle, + ticketResponse.getTicketQuantity(), + ticketResponse.getTotalPrice(), + kakaoPayProperties.getApprovalUrl(), + kakaoPayProperties.getCancelUrl(), + kakaoPayProperties.getFailUrl() + ); + KakaoPayReadyResponse response = kakaoPayClient.ready(kakaoPayProperties.getAuthorization(), request); + + PaymentCache paymentReady = new PaymentCache( + userResponse.getEmail(), + movieTitle, + response.getTid(), + ticketIds, + userAlternateId, + paymentNumber + ); + + paymentCacheRepository.findByEmail(userResponse.getEmail()) + .ifPresentOrElse( + paymentCache -> ErrorCode.throwBadRequestPaymentReady() + , () -> paymentCacheRepository.save(paymentReady) + ); + + return new PaymentReadyDTO(response); + } + + @Override + @Transactional + public PaymentCompleteDTO complete(@NotEmpty String email, @NotEmpty String pgToken) { + PaymentCache paymentCache = paymentCacheRepository.findByEmail(email) + .orElseThrow(ErrorCode::throwBadRequestPaymentComplete); + + String paymentNumberToString = paymentCache.getPaymentNumber().toString(); + String userAlternateIdToString = paymentCache.getUserAlternateId().toString(); + KakaoPayApproveRequest kakaoPayApproveRequest = new KakaoPayApproveRequest( + paymentCache.getTid(), + paymentNumberToString, + userAlternateIdToString, + pgToken + ); + KakaoPayApproveResponse kakaoPayApproveResponse = kakaoPayClient.approve( + kakaoPayProperties.getAuthorization(), + kakaoPayApproveRequest + ); + + Payment payment = new Payment(paymentCache, PaymentType.KAKAO_PAY, PaymentStatus.SOLD, kakaoPayApproveResponse.getTotalAmount()); + payment = paymentRepository.save(payment); + + movieClient.ticketSold(new TicketSoldRequest(payment.getId(), paymentCache.getTicketIds())); + paymentCacheRepository.delete(paymentCache); + + return new PaymentCompleteDTO(email, kakaoPayApproveResponse); + } + + @Override + @Transactional + public PaymentCancelDTO cancel(@NotEmpty String email) { + PaymentCache paymentCache = paymentCacheRepository.findByEmail(email) + .orElseThrow(ErrorCode::throwBadRequestPaymentComplete); + + movieClient.ticketCancel(new TicketCancelRequest(paymentCache.getTicketIds())); + paymentCacheRepository.delete(paymentCache); + + return new PaymentCancelDTO(paymentCache); + } + } diff --git a/server/src/main/java/com/ticketing/server/payment/service/dto/CreatePaymentDTO.java b/server/src/main/java/com/ticketing/server/payment/service/dto/CreatePaymentDTO.java deleted file mode 100644 index d839c95..0000000 --- a/server/src/main/java/com/ticketing/server/payment/service/dto/CreatePaymentDTO.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.ticketing.server.payment.service.dto; - -import com.ticketing.server.payment.domain.Payment; -import com.ticketing.server.payment.domain.PaymentStatus; -import com.ticketing.server.payment.domain.PaymentType; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class CreatePaymentDTO { - - private final Long userAlternateId; - private final String movieTitle; - private final PaymentType type; - private final PaymentStatus status; - private final String paymentNumber; - private final Integer totalPrice; - - public Payment toEntity() { - return new Payment - ( - this.getUserAlternateId(), - this.getMovieTitle(), - this.getType(), - this.getStatus(), - this.getPaymentNumber(), - this.getTotalPrice() - ); - } - -} diff --git a/server/src/main/java/com/ticketing/server/payment/service/dto/PaymentCancelDTO.java b/server/src/main/java/com/ticketing/server/payment/service/dto/PaymentCancelDTO.java new file mode 100644 index 0000000..bfd4be2 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/service/dto/PaymentCancelDTO.java @@ -0,0 +1,32 @@ +package com.ticketing.server.payment.service.dto; + +import com.ticketing.server.global.redis.PaymentCache; +import com.ticketing.server.payment.application.response.PaymentCancelResponse; +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class PaymentCancelDTO { + + private final String email; + private final String movieTitle; + private final String tid; + private final List ticketIds; + + public PaymentCancelDTO(PaymentCache paymentCache) { + this( + paymentCache.getEmail(), + paymentCache.getMovieTitle(), + paymentCache.getTid(), + paymentCache.getTicketIds() + ); + } + + public PaymentCancelResponse toResponse() { + return new PaymentCancelResponse(email, movieTitle, tid, ticketIds); + } + +} diff --git a/server/src/main/java/com/ticketing/server/payment/service/dto/PaymentCompleteDTO.java b/server/src/main/java/com/ticketing/server/payment/service/dto/PaymentCompleteDTO.java new file mode 100644 index 0000000..a8d7746 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/service/dto/PaymentCompleteDTO.java @@ -0,0 +1,33 @@ +package com.ticketing.server.payment.service.dto; + +import com.ticketing.server.payment.api.dto.response.KakaoPayApproveResponse; +import com.ticketing.server.payment.application.response.PaymentCompleteResponse; +import javax.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class PaymentCompleteDTO { + + private final String email; + private final String tid; + private final String movieTitle; + private final Integer quantity; + private final Integer totalAmount; + + public PaymentCompleteDTO(@NotEmpty String email, KakaoPayApproveResponse kakaoPayApproveResponse) { + this( + email, + kakaoPayApproveResponse.getTid(), + kakaoPayApproveResponse.getItemName(), + kakaoPayApproveResponse.getQuantity(), + kakaoPayApproveResponse.getTotalAmount() + ); + } + + public PaymentCompleteResponse toResponse() { + return new PaymentCompleteResponse(email, tid, movieTitle, quantity, totalAmount); + } + +} diff --git a/server/src/main/java/com/ticketing/server/payment/service/dto/PaymentReadyDTO.java b/server/src/main/java/com/ticketing/server/payment/service/dto/PaymentReadyDTO.java new file mode 100644 index 0000000..cc64273 --- /dev/null +++ b/server/src/main/java/com/ticketing/server/payment/service/dto/PaymentReadyDTO.java @@ -0,0 +1,40 @@ +package com.ticketing.server.payment.service.dto; + +import com.ticketing.server.payment.api.dto.response.KakaoPayReadyResponse; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class PaymentReadyDTO { + + // 결제 고유 번호 + private String tid; + + // 요청 클라이언트가 모바일 앱일 경우 + private String nextRedirectAppUrl; + + // 요청 클라이언트가 모바일 웹일 경우 + private String nextRedirectMobileUrl; + + // 요청 클라이언트가 PC 웹일 경우 + private String nextRedirectPcUrl; + + // 카카오페이 결제 화면으로 이동하는 Android 앱 스킴 + private String androidAppScheme; + + // 카카오페이 결제 화면으로 이동하는 IOS 앱 스킴 + private String iosAppScheme; + + public PaymentReadyDTO(KakaoPayReadyResponse response) { + this( + response.getTid(), + response.getNextRedirectAppUrl(), + response.getNextRedirectMobileUrl(), + response.getNextRedirectPcUrl(), + response.getAndroidAppScheme(), + response.getIosAppScheme() + ); + } +} diff --git a/server/src/main/java/com/ticketing/server/payment/service/interfaces/PaymentApisService.java b/server/src/main/java/com/ticketing/server/payment/service/interfaces/PaymentApisService.java index 9120688..50afb7a 100644 --- a/server/src/main/java/com/ticketing/server/payment/service/interfaces/PaymentApisService.java +++ b/server/src/main/java/com/ticketing/server/payment/service/interfaces/PaymentApisService.java @@ -1,9 +1,22 @@ package com.ticketing.server.payment.service.interfaces; +import com.ticketing.server.global.validator.constraints.NotEmptyCollection; +import com.ticketing.server.payment.service.dto.PaymentCancelDTO; +import com.ticketing.server.payment.service.dto.PaymentCompleteDTO; import com.ticketing.server.payment.service.dto.PaymentDetailDTO; +import com.ticketing.server.payment.service.dto.PaymentReadyDTO; +import java.util.List; +import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; public interface PaymentApisService { PaymentDetailDTO findPaymentDetail(@NotNull Long paymentId); + + PaymentReadyDTO ready(@NotEmptyCollection List ticketIds); + + PaymentCompleteDTO complete(@NotEmpty String email, @NotEmpty String pgToken); + + PaymentCancelDTO cancel(@NotEmpty String email); + } diff --git a/server/src/main/java/com/ticketing/server/user/application/UserController.java b/server/src/main/java/com/ticketing/server/user/application/UserController.java index e2387f4..0a97721 100644 --- a/server/src/main/java/com/ticketing/server/user/application/UserController.java +++ b/server/src/main/java/com/ticketing/server/user/application/UserController.java @@ -9,6 +9,7 @@ import com.ticketing.server.user.application.request.UserChangePasswordRequest; import com.ticketing.server.user.application.request.UserDeleteRequest; import com.ticketing.server.user.application.response.PaymentsResponse; import com.ticketing.server.user.application.response.SignUpResponse; +import com.ticketing.server.user.application.response.UserChangeGradeResponse; import com.ticketing.server.user.application.response.UserChangePasswordResponse; import com.ticketing.server.user.application.response.UserDeleteResponse; import com.ticketing.server.user.application.response.UserDetailResponse; diff --git a/server/src/main/java/com/ticketing/server/user/application/UserChangeGradeResponse.java b/server/src/main/java/com/ticketing/server/user/application/response/UserChangeGradeResponse.java similarity index 82% rename from server/src/main/java/com/ticketing/server/user/application/UserChangeGradeResponse.java rename to server/src/main/java/com/ticketing/server/user/application/response/UserChangeGradeResponse.java index 93b5878..1c3c2b1 100644 --- a/server/src/main/java/com/ticketing/server/user/application/UserChangeGradeResponse.java +++ b/server/src/main/java/com/ticketing/server/user/application/response/UserChangeGradeResponse.java @@ -1,4 +1,4 @@ -package com.ticketing.server.user.application; +package com.ticketing.server.user.application.response; import com.ticketing.server.user.domain.UserGrade; import lombok.AllArgsConstructor; diff --git a/server/src/main/java/com/ticketing/server/user/domain/ChangeGradeDTO.java b/server/src/main/java/com/ticketing/server/user/domain/ChangeGradeDTO.java index 249b782..4912169 100644 --- a/server/src/main/java/com/ticketing/server/user/domain/ChangeGradeDTO.java +++ b/server/src/main/java/com/ticketing/server/user/domain/ChangeGradeDTO.java @@ -1,6 +1,6 @@ package com.ticketing.server.user.domain; -import com.ticketing.server.user.application.UserChangeGradeResponse; +import com.ticketing.server.user.application.response.UserChangeGradeResponse; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/server/src/main/java/com/ticketing/server/user/service/UserServiceImpl.java b/server/src/main/java/com/ticketing/server/user/service/UserServiceImpl.java index af8fdac..b07cab3 100644 --- a/server/src/main/java/com/ticketing/server/user/service/UserServiceImpl.java +++ b/server/src/main/java/com/ticketing/server/user/service/UserServiceImpl.java @@ -2,7 +2,7 @@ package com.ticketing.server.user.service; import com.ticketing.server.global.exception.ErrorCode; import com.ticketing.server.user.domain.ChangeGradeDTO; -import com.ticketing.server.user.domain.SequenceGenerator; +import com.ticketing.server.global.dto.SequenceGenerator; import com.ticketing.server.user.domain.User; import com.ticketing.server.user.domain.UserGrade; import com.ticketing.server.user.domain.repository.UserRepository; diff --git a/server/src/main/resources/application-local.yml b/server/src/main/resources/application-local.yml index a506772..4990469 100644 --- a/server/src/main/resources/application-local.yml +++ b/server/src/main/resources/application-local.yml @@ -12,3 +12,13 @@ spring: format_sql: true hibernate: ddl-auto: create + +api: + kakao: + pay: + url: http://localhost:8080 + approval: /api/payments/complete + cancel: /api/payments/cancel + fail: /api/payments/cancel + prefix: KakaoAK + key: b2472272b813500c51a25e1422261676 diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml index d7f738e..ca45a47 100644 --- a/server/src/main/resources/application.yml +++ b/server/src/main/resources/application.yml @@ -23,6 +23,10 @@ spring: matching-strategy: ant_path_matcher throw-exception-if-no-handler-found: true + config: + import: "optional:configserver:" + + jasypt: encryptor: bean: jasyptStringEncryptor @@ -39,3 +43,7 @@ springfox: documentation: swagger: use-model-v3: false + +logging: + level: + com.ticketing.server.payment.api.KakaoPayClient: DEBUG diff --git a/server/src/test/java/com/ticketing/server/movie/domain/TicketTest.java b/server/src/test/java/com/ticketing/server/movie/domain/TicketTest.java index d1cf9fb..8c608f7 100644 --- a/server/src/test/java/com/ticketing/server/movie/domain/TicketTest.java +++ b/server/src/test/java/com/ticketing/server/movie/domain/TicketTest.java @@ -4,6 +4,7 @@ import static com.ticketing.server.movie.domain.MovieTimeTest.MOVIE_TIMES; import static com.ticketing.server.movie.domain.SeatTest.SEATS_BY_THEATER; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; import com.ticketing.server.global.exception.ErrorCode; import com.ticketing.server.global.exception.TicketingException; @@ -65,4 +66,109 @@ public class TicketTest { .isEqualTo(ErrorCode.DUPLICATE_PAYMENT); } + @Test + @DisplayName("좌석 예약 성공") + void makeReservationSuccess() { + // given + Ticket ticket = tickets.get(0); + + // when + ticket = ticket.makeReservation(); + + // then + assertThat(ticket.getStatus()).isEqualTo(TicketStatus.RESERVATION); + } + + @Test + @DisplayName("좌석 예약 실패 - 이미 예약중인 좌석인 경우") + void makeReservationFail_Reservation() { + // given + Ticket ticket = tickets.get(0); + ticket.makeReservation(); + + // when + // then + assertThatThrownBy(ticket::makeReservation) + .isInstanceOf(TicketingException.class) + .extracting("errorCode") + .isEqualTo(ErrorCode.DUPLICATE_PAYMENT); + } + + @Test + @DisplayName("좌석 예약 실패 - 이미 판매된 좌석인 경우") + void makeReservationFail_Sold() { + // given + Ticket ticket = tickets.get(0); + ticket.makeSold(123L); + + // when + // then + assertThatThrownBy(ticket::makeReservation) + .isInstanceOf(TicketingException.class) + .extracting("errorCode") + .isEqualTo(ErrorCode.DUPLICATE_PAYMENT); + } + + @Test + @DisplayName("좌석 구매 성공") + void makeSoldSuccess() { + // given + Ticket ticket = tickets.get(0); + + // when + ticket.makeSold(123L); + + // then + assertAll( + () -> assertThat(ticket.getStatus()).isEqualTo(TicketStatus.SOLD), + () -> assertThat(ticket.getPaymentId()).isEqualTo(123L) + ); + } + + @Test + @DisplayName("좌석 구매 실패 - 이미 판매된 좌석") + void makeSoldFail() { + // given + Ticket ticket = tickets.get(0); + + // when + ticket.makeSold(123L); + + // then + assertThatThrownBy(() -> ticket.makeSold(123L)) + .isInstanceOf(TicketingException.class) + .extracting("errorCode") + .isEqualTo(ErrorCode.DUPLICATE_PAYMENT); + } + + @Test + @DisplayName("좌석 취소 성공") + void cancelSuccess() { + // given + Ticket ticket = tickets.get(0); + + // when + ticket.makeReservation(); + ticket.cancel(); + + // then + assertThat(ticket.getStatus()).isEqualTo(TicketStatus.SALE); + } + + @Test + @DisplayName("좌석 취소 실패") + void cancelFail() { + // given + Ticket ticket = tickets.get(0); + + // when + ticket.makeSold(123L); + + // then + assertThatThrownBy(() -> ticket.cancel()) + .isInstanceOf(TicketingException.class) + .extracting("errorCode") + .isEqualTo(ErrorCode.BAD_REQUEST_PAYMENT_CANCEL); + } + } diff --git a/server/src/test/java/com/ticketing/server/movie/service/TicketServiceImplTest.java b/server/src/test/java/com/ticketing/server/movie/service/TicketServiceImplTest.java index 729d845..c11f49f 100644 --- a/server/src/test/java/com/ticketing/server/movie/service/TicketServiceImplTest.java +++ b/server/src/test/java/com/ticketing/server/movie/service/TicketServiceImplTest.java @@ -11,6 +11,7 @@ import com.ticketing.server.global.exception.TicketingException; import com.ticketing.server.movie.domain.Ticket; import com.ticketing.server.movie.domain.repository.TicketRepository; import com.ticketing.server.movie.service.dto.TicketDetailsDTO; +import com.ticketing.server.movie.service.dto.TicketsReservationDTO; import com.ticketing.server.payment.service.dto.TicketDetailDTO; import java.util.Collections; import java.util.List; @@ -62,4 +63,39 @@ class TicketServiceImplTest { ); } + @Test + @DisplayName("티켓목록 예약으로 변경 시 조회된 갯수랑 다른 경우") + void ticketReservationFail() { + // given + List tickets = setupTickets(); + List list = List.of(tickets.get(0), tickets.get(1), tickets.get(2)); + List ids = List.of(0L, 1L, 2L, 10000L); + + when(ticketRepository.findTicketFetchJoinByTicketIds(ids)).thenReturn(list); + + // when + // then + assertThatThrownBy(() -> ticketService.ticketReservation(ids)) + .isInstanceOf(TicketingException.class) + .extracting("errorCode") + .isEqualTo(ErrorCode.INVALID_TICKET_ID); + } + + @Test + @DisplayName("티켓목록 예약으로 변경 완료") + void ticketReservationSuccess() { + // given + List tickets = setupTickets(); + List list = List.of(tickets.get(0), tickets.get(1), tickets.get(2)); + List ids = List.of(0L, 1L, 2L); + + when(ticketRepository.findTicketFetchJoinByTicketIds(ids)).thenReturn(list); + + // when + TicketsReservationDTO ticketReservationsDto = ticketService.ticketReservation(ids); + + // then + assertThat(ticketReservationsDto.getTicketReservationDtoList()).hasSize(3); + } + } diff --git a/server/src/test/java/com/ticketing/server/payment/domain/PaymentTest.java b/server/src/test/java/com/ticketing/server/payment/domain/PaymentTest.java index 87f4731..f3ad1e3 100644 --- a/server/src/test/java/com/ticketing/server/payment/domain/PaymentTest.java +++ b/server/src/test/java/com/ticketing/server/payment/domain/PaymentTest.java @@ -3,6 +3,6 @@ package com.ticketing.server.payment.domain; public class PaymentTest { public static final Payment PAYMENT_USER_1 - = new Payment(1L, 111L, "탑건: 매버릭", PaymentType.KAKAO_PAY, PaymentStatus.COMPLETED, "2022-0710-4142", 30_000); + = new Payment(1L, 111L, "탑건: 매버릭", PaymentType.KAKAO_PAY, PaymentStatus.SOLD, "2022-0710-4142", 30_000); } diff --git a/server/src/test/java/com/ticketing/server/payment/service/PaymentServiceImplTest.java b/server/src/test/java/com/ticketing/server/payment/service/PaymentServiceImplTest.java index f0a5791..8a124a0 100644 --- a/server/src/test/java/com/ticketing/server/payment/service/PaymentServiceImplTest.java +++ b/server/src/test/java/com/ticketing/server/payment/service/PaymentServiceImplTest.java @@ -1,13 +1,13 @@ package com.ticketing.server.payment.service; -import static com.ticketing.server.payment.domain.PaymentStatus.COMPLETED; +import static com.ticketing.server.payment.domain.PaymentStatus.SOLD; import static com.ticketing.server.payment.domain.PaymentType.KAKAO_PAY; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; +import com.ticketing.server.global.redis.PaymentCache; import com.ticketing.server.payment.domain.Payment; import com.ticketing.server.payment.domain.repository.PaymentRepository; -import com.ticketing.server.payment.service.dto.CreatePaymentDTO; import com.ticketing.server.payment.service.dto.SimplePaymentsDTO; import java.util.Arrays; import java.util.Collections; @@ -50,8 +50,32 @@ class PaymentServiceImplTest { void findSimplePaymentsSuccess(Long userAlternateId) { // given List payments = Arrays.asList( - new CreatePaymentDTO(userAlternateId, "범죄도시2", KAKAO_PAY, COMPLETED, "004-323-77542", 15_000).toEntity(), - new CreatePaymentDTO(userAlternateId, "토르", KAKAO_PAY, COMPLETED, "004-323-77544", 30_000).toEntity() + new Payment( + new PaymentCache( + "", + "범죄도시2", + "T2d03c9130bf237a97001", + List.of(3L), + userAlternateId, + 1241242343245L + ), + KAKAO_PAY, + SOLD, + 15_000 + ), + new Payment( + new PaymentCache( + "", + "토르", + "T2d03c9130bf237a97002", + List.of(3L), + userAlternateId, + 12412343212445L + ), + KAKAO_PAY, + SOLD, + 30_000 + ) ); when(paymentRepository.findByUserAlternateId(userAlternateId)).thenReturn(payments); diff --git a/server/src/test/java/com/ticketing/server/user/domain/SnowflakeTest.java b/server/src/test/java/com/ticketing/server/user/domain/SnowflakeTest.java index 463faa9..6f17e2a 100644 --- a/server/src/test/java/com/ticketing/server/user/domain/SnowflakeTest.java +++ b/server/src/test/java/com/ticketing/server/user/domain/SnowflakeTest.java @@ -3,6 +3,7 @@ package com.ticketing.server.user.domain; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; +import com.ticketing.server.global.dto.Snowflake; import java.time.Instant; import java.util.Arrays; import java.util.HashSet; diff --git a/server/src/test/java/com/ticketing/server/user/service/UserApisServiceImplTest.java b/server/src/test/java/com/ticketing/server/user/service/UserApisServiceImplTest.java index a584b29..4749b70 100644 --- a/server/src/test/java/com/ticketing/server/user/service/UserApisServiceImplTest.java +++ b/server/src/test/java/com/ticketing/server/user/service/UserApisServiceImplTest.java @@ -1,14 +1,15 @@ package com.ticketing.server.user.service; -import static com.ticketing.server.payment.domain.PaymentStatus.COMPLETED; +import static com.ticketing.server.payment.domain.PaymentStatus.SOLD; import static com.ticketing.server.payment.domain.PaymentType.KAKAO_PAY; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import com.ticketing.server.global.redis.PaymentCache; import com.ticketing.server.payment.application.response.SimplePaymentsResponse; -import com.ticketing.server.payment.service.dto.CreatePaymentDTO; +import com.ticketing.server.payment.domain.Payment; import com.ticketing.server.payment.service.dto.SimplePaymentDTO; import com.ticketing.server.user.api.PaymentClient; import com.ticketing.server.user.domain.User; @@ -68,9 +69,35 @@ class UserApisServiceImplTest { // given List payments = Arrays.asList( new SimplePaymentDTO( - new CreatePaymentDTO(1L, "범죄도시2", KAKAO_PAY, COMPLETED, "004-323-77542", 15_000).toEntity()), + new Payment( + new PaymentCache( + user.getEmail(), + "범죄도시2", + "T2d03c9130bf237a9700", + List.of(1L, 2L), + user.getAlternateId(), + 124124231513245L + ), + KAKAO_PAY, + SOLD, + 15_000 + ) + ), new SimplePaymentDTO( - new CreatePaymentDTO(1L, "토르", KAKAO_PAY, COMPLETED, "004-323-77544", 30_000).toEntity()) + new Payment( + new PaymentCache( + user.getEmail(), + "범죄도시2", + "T2d03c9130bf237a97001", + List.of(3L), + user.getAlternateId(), + 1241242343245L + ), + KAKAO_PAY, + SOLD, + 15_000 + ) + ) ); when(userService.findNotDeletedUserByEmail("ticketing@gmail.com")).thenReturn(user); diff --git a/server/src/test/java/com/ticketing/server/user/service/UserServiceImplTest.java b/server/src/test/java/com/ticketing/server/user/service/UserServiceImplTest.java index 4da4020..e5112d0 100644 --- a/server/src/test/java/com/ticketing/server/user/service/UserServiceImplTest.java +++ b/server/src/test/java/com/ticketing/server/user/service/UserServiceImplTest.java @@ -8,7 +8,7 @@ import static org.mockito.Mockito.when; import com.ticketing.server.global.exception.ErrorCode; import com.ticketing.server.global.exception.TicketingException; -import com.ticketing.server.user.domain.SequenceGenerator; +import com.ticketing.server.global.dto.SequenceGenerator; import com.ticketing.server.user.domain.User; import com.ticketing.server.user.domain.UserGrade; import com.ticketing.server.user.domain.repository.UserRepository;