Compare commits

...

22 Commits

Author SHA1 Message Date
손창현
220d43f5fd feat: movie controller integration test (#96) 2022-08-31 12:21:57 +09:00
Kim DongHyo
fd5b443d48 [User] AuthController 통합테스트 추가 구현 (#94)
* feat: 리프레쉬토큰 및 로그아웃 통합테스트 작성

* refactor: import 정리
2022-08-16 12:53:43 +09:00
손창현
975e03b7bd add: prometheus (#92) 2022-08-10 14:42:41 +09:00
Kim DongHyo
657c9e7f7d feat: 티켓 구매, 환불 메서드 분산 락 적용 (#86)
* feat: 티켓 구매, 환불 메서드 분산 락 적용

* refactor: aop 를 통한 lock 검증 메서드 분리

* fix: @TicketLock annotations delete
2022-08-05 17:54:45 +09:00
Kim DongHyo
c5b779fda7 fix: 리프레쉬 토큰 업데이트 시 ROLE_이 중복으로 붙여서 나오는 부분 수정 (#90) 2022-08-01 23:45:09 +09:00
손창현
b7057cfc73 Feature/dockerize (#88)
* add: pkcs12 ssl key

* add: application-prod.yml

* add: Dockerfile

* add: redis host, port

* fix: 하단 공백 추가

* refactor: set ddl-auto: validate
2022-08-01 18:32:43 +09:00
손창현
42a00d20d0 feat: MovieTimeServiceTest - test registering movie time (#83) 2022-07-20 01:16:28 +09:00
Kim DongHyo
b3842d93b4 [User] 유저 컨트롤러 API 통합테스트 작성 (#84)
* test: 유저 통합테스트 추가

* test: JwtFilter 테스트케이스 추가

* refactor: 매직넘버 제거

* refactor: 회원탈퇴 / 패스워드 변경 검증 로직 수정
2022-07-20 01:06:22 +09:00
손창현
58bed5565f Feature/delete movie test (#79)
* feat: MovieServiceImplTest - test registering movie

* feat: MovieServiceImplTest - test deleteing movie

* feat: MovieServiceImplTest - test code for checking if exceptions occur

* refactor: MovieRepository NativeQuery -> JPQL
2022-07-19 02:13:13 +09:00
dongHyo
28874b1b1c Merge remote-tracking branch 'origin/develop' into feature/removeCollectionDTO 2022-07-18 17:57:02 +09:00
dongHyo
656e5e25e8 refactor: Ticket removed ListDTO 2022-07-18 17:56:53 +09:00
손창현
f1382597de Refactor/list dto (#78)
* refactor: removed MovieListDTO:

* refactor: removed MovieTimeListDTO

* refactor: MovieTimeServiceImplTest - removed MovieTimeListDto
2022-07-18 17:16:00 +09:00
Kim DongHyo
a2cfd7ab15 [Payment] 환불 구현 (#76)
* feat: 본인결제내역 환불 구현

* feat: 관리자 환불 구현

* refactor: 피드백 반영
2022-07-17 23:40:28 +09:00
Kim DongHyo
61f6636653 Update README.md 2022-07-15 17:54:03 +09:00
손창현
a1579cfe13 Feature/get ticket list (#75)
* add: TicketListDTO

* add: TicketDTO

* add: TicketListResponse

* add: findValidTickets(Long movieTimeId) in ticket repository

* add: MOVIE_TIME_NOT_FOUND exception

* feat: get ticket list API

* add: secured USER for ticket APIs

* refactor: 코드 정렬
2022-07-15 13:22:32 +09:00
Kim DongHyo
c760d66cf6 feat: 영화티켓 예매 구현 (카카오페이API 연동) (#74)
* feat: 영화티켓 예매 구현 (카카오페이API 연동)

* fix: 테스트 값 제거
2022-07-15 04:32:20 +09:00
손창현
68ad0ce910 Feature/register movie time (#72)
* refactor: get movie times API

* refactor: get movie times test

* fix: removed unnecessary import

* add: RegisteredMovieTimeDTO

* add: MovieTimeRegisterRequest

* add: THEATER_NOT_FOUND exception

* add: DUPLICATE_MOVIE_TIME exception

* feat: register movie time API

* fix: 공백 탭으로 변경
2022-07-15 02:47:39 +09:00
손창현
d80a370232 Feature/delete movie (#71)
* refactor: set movie title unique -> not unique

* add: findValidMovieWithTitle() in MovieRepository

* refactor: changed MovieTitleResponse -> MovieInfoResponse

* refactor: register Movie API

* add: throwDeletedMovie() exception

* add: deleteMovie() in movieService

* refactor: get movie list API

* refactor: movieService test code

* feat: delete movie API
2022-07-14 12:25:52 +09:00
Kim DongHyo
12f1d6b9be fix: 브런치 merge 중 import 가 잘못되서 수정 (#70) 2022-07-13 14:13:48 +09:00
Kim DongHyo
db5e8272b6 Update README.md 2022-07-12 23:18:47 +09:00
Kim DongHyo
9780a84609 refactor: 정적팩토리 -> 생성자 변경 (#69)
Co-authored-by: 손창현 <gganzii1215@gmail.com>
2022-07-12 18:51:44 +09:00
Kim DongHyo
7283eeb5d3 feat: 권한변경 API 구현 (#67) 2022-07-12 18:31:18 +09:00
172 changed files with 4447 additions and 516 deletions

View File

@@ -37,6 +37,7 @@
- [패키지 구조](https://github.com/f-lab-edu/Ticketing/wiki#-%ED%8C%A8%ED%82%A4%EC%A7%80-%EA%B5%AC%EC%A1%B0)
- [Use Case](https://github.com/f-lab-edu/Ticketing/wiki/Use-Case)
- [Prototype](https://github.com/f-lab-edu/Ticketing/wiki/Prototype)
- [Issue Posting](https://github.com/f-lab-edu/Ticketing/wiki/Issue-Posting)
<br/>
@@ -90,7 +91,7 @@ erDiagram
bigint seat_id FK "좌석 ID"
bigint movie_time_id FK "상영시간표 ID"
bigint payment_id "결제 ID"
varchar status "상태 - 매가능/예약진행중/판매완료"
varchar status "상태 - 매가능/예약/"
int ticket_price "가격"
datetime deleted_at "삭제일시"
datetime created_at "등록일시"
@@ -100,10 +101,11 @@ erDiagram
PAYMENT {
bigint id PK "결제 ID"
bigint user_alternate_id "유저 대체ID"
varchar tid "카카오페이 결제고유번호"
varchar movie_title "영화제목"
varchar type "결제 타입 - 예) 네이버페이, 카카오페이"
varchar status "상태 - 완료/환불/실패"
varchar failed_message "실패사유 - 컬럼명을 알아보기 쉬운가?"
varchar status "상태 - 완료/환불"
varchar failed_message "실패사유"
varchar payment_number "예매번호"
int total_price "결제 금액"
datetime deleted_at "삭제일시"

1
server/.gitignore vendored
View File

@@ -6,6 +6,7 @@
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
HELP.md
bin/**
# User-specific stuff
.idea/**/workspace.xml

12
server/Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM openjdk:11-jre-slim
ENV APP_HOME=/usr/app/
WORKDIR $APP_HOME
COPY build/libs/server-0.0.1-SNAPSHOT.jar application.jar
EXPOSE 8443
EXPOSE 8080
CMD ["java", "-jar", "application.jar"]

View File

@@ -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,10 @@ 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")
implementation("io.micrometer:micrometer-core")
implementation("io.micrometer:micrometer-registry-prometheus")
modules {
module("org.springframework.boot:spring-boot-starter-logging") {
@@ -58,6 +64,12 @@ dependencies {
testImplementation("org.springframework.security:spring-security-test")
}
dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
}
}
tasks.withType<Test> {
useJUnitPlatform()
}

View File

@@ -0,0 +1,15 @@
package com.ticketing.server.global.config;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import org.springframework.security.test.context.support.WithSecurityContext;
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithAuthUserSecurityContextFactory.class)
public @interface WithAuthUser {
String email();
String role();
}

View File

@@ -0,0 +1,27 @@
package com.ticketing.server.global.config;
import java.util.List;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.test.context.support.WithSecurityContextFactory;
public class WithAuthUserSecurityContextFactory implements WithSecurityContextFactory<WithAuthUser> {
@Override
public SecurityContext createSecurityContext(WithAuthUser annotation) {
String email = annotation.email();
String role = annotation.role();
List<SimpleGrantedAuthority> authorities = List.of(new SimpleGrantedAuthority(role));
User authUser = new User(email, "", authorities);
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(authUser, "", authorities);
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(token);
return context;
}
}

View File

@@ -0,0 +1,56 @@
package com.ticketing.server.movie.aop;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class TicketLockAspectTest {
@Autowired
private TicketLockAspect ticketLockAspect;
@Test
@DisplayName("티켓 lock 동시성 체크")
@SuppressWarnings({"java:S5960"})
void ticketMultiThread() throws InterruptedException {
// given
ExecutorService executorService = Executors.newFixedThreadPool(2);
CountDownLatch latch = new CountDownLatch(2);
List<String> lockIds = List.of("TicketLock:1", "TicketLock:2", "TicketLock:3");
AtomicBoolean result1 = new AtomicBoolean(Boolean.TRUE);
AtomicBoolean result2 = new AtomicBoolean(Boolean.TRUE);
// when
executorService.execute(() -> {
result1.set(ticketLockAspect.isEveryTicketIdLock(lockIds));
latch.countDown();
});
executorService.execute(() -> {
result2.set(ticketLockAspect.isEveryTicketIdLock(List.of("TicketLock:1")));
latch.countDown();
});
latch.await();
// then
Long unlockCount = ticketLockAspect.ticketIdsUnlock(lockIds);
assertAll(
() -> assertThat(result1).isNotEqualTo(result2),
() -> assertThat(unlockCount > 0).isTrue()
);
}
}

View File

@@ -0,0 +1,202 @@
package com.ticketing.server.movie.application;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ticketing.server.global.config.WithAuthUser;
import com.ticketing.server.movie.application.request.MovieDeleteRequest;
import com.ticketing.server.movie.application.request.MovieRegisterRequest;
import com.ticketing.server.user.domain.UserGrade.ROLES;
import org.json.simple.JSONObject;
import org.json.simple.JSONArray;
import org.json.simple.parser.JSONParser;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;
@SpringBootTest
@Transactional
public class MovieControllerTest {
@Autowired
ObjectMapper mapper;
@Autowired
WebApplicationContext context;
MockMvc mvc;
JSONParser jsonParser = new JSONParser();
private static final String MOVIES_URL = "/api/movies";
private static final Long RUNNING_TIME = 100L;
private static final String MOVIE_TITLE = "등록할 영화";
private static final String TITLE = "$.title";
private static final String MOVIE_DTOS = "$.movieDtos";
@BeforeEach
void init() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
@Test
@DisplayName("영화 등록 성공")
@WithAuthUser(email = "staff@ticketing.com", role = ROLES.STAFF)
void movieRegisterSuccess() throws Exception {
MovieRegisterRequest request = new MovieRegisterRequest(MOVIE_TITLE, RUNNING_TIME);
ResultActions resultActions = mvc.perform(post(MOVIES_URL)
.content(mapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON));
resultActions
.andExpect(status().isOk())
.andExpect(content().contentType(APPLICATION_JSON))
.andExpect(jsonPath(TITLE).value(MOVIE_TITLE));
}
@Test
@DisplayName("영화 등록 실패 - 권한 부족")
@WithAuthUser(email = "user@ticketing.com", role = ROLES.USER)
void movieRegisterFailWithLowAuthority() throws Exception {
MovieRegisterRequest request = new MovieRegisterRequest(MOVIE_TITLE, RUNNING_TIME);
ResultActions resultActions = mvc.perform(post(MOVIES_URL)
.content(mapper.writeValueAsString(request))
.contentType(APPLICATION_JSON));
resultActions.andExpect(status().isForbidden());
}
@Test
@DisplayName("영화 등록 실패 - 인자값 검증 실패")
@WithAuthUser(email = "staff@ticketing.com", role = ROLES.ADMIN)
void movieRegisterFailWithWrongParameter() throws Exception {
MovieRegisterRequest requestWithNullRunningTime = new MovieRegisterRequest(MOVIE_TITLE, null);
// 1. 상영 시간 null
ResultActions resultActions = mvc.perform(post(MOVIES_URL)
.content(mapper.writeValueAsString(requestWithNullRunningTime))
.contentType(APPLICATION_JSON));
resultActions.andExpect(status().isBadRequest());
// 2. 영화 제목 null
MovieRegisterRequest requestWithNullTitle = new MovieRegisterRequest(null, RUNNING_TIME);
resultActions = mvc.perform(post(MOVIES_URL)
.content(mapper.writeValueAsString(requestWithNullTitle))
.contentType(APPLICATION_JSON));
resultActions.andExpect(status().isBadRequest());
// 3. 영화 제목 ""
MovieRegisterRequest requestWithoutTitle = new MovieRegisterRequest("", RUNNING_TIME);
resultActions = mvc.perform(post(MOVIES_URL)
.content(mapper.writeValueAsString(requestWithoutTitle))
.contentType(APPLICATION_JSON));
resultActions.andExpect(status().isBadRequest());
}
@Test
@DisplayName("영화 등록 실패 - 같은 영화 중복 등록")
@WithAuthUser(email = "staff@ticketing.com", role = ROLES.ADMIN)
void movieRegisterFailWithSameMovie() throws Exception {
// given
MovieRegisterRequest request = new MovieRegisterRequest(MOVIE_TITLE, RUNNING_TIME);
mvc.perform(post(MOVIES_URL)
.content(mapper.writeValueAsString(request))
.contentType(APPLICATION_JSON))
.andExpect(status().isOk());
// when
ResultActions resultActions = mvc.perform(post(MOVIES_URL)
.content(mapper.writeValueAsString(request))
.contentType(APPLICATION_JSON));
// then
resultActions.andExpect(status().isConflict());
}
@Test
@DisplayName("영화 삭제 성공")
@WithAuthUser(email = "staff@ticketing.com", role = ROLES.ADMIN)
void movieDeleteSuccess() throws Exception {
// given
MovieRegisterRequest request = new MovieRegisterRequest(MOVIE_TITLE, RUNNING_TIME);
mvc.perform(post(MOVIES_URL)
.content(mapper.writeValueAsString(request))
.contentType(APPLICATION_JSON))
.andExpect(status().isOk());
// when
// 1. 영화 조회 - 삭제할 영화 ID 뽑기
ResultActions resultActions = mvc.perform(get(MOVIES_URL)
.contentType(APPLICATION_JSON));
resultActions
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath(MOVIE_DTOS).isNotEmpty());
MvcResult result = resultActions.andReturn();
Object obj = jsonParser.parse(result.getResponse().getContentAsString());
JSONObject jsonObject = (JSONObject) obj;
Object object = jsonObject.get("movieDtos");
JSONArray jsonArray = (JSONArray) object;
JSONObject jsonObj = (JSONObject) jsonArray.get(0);
Long movieId = (Long) jsonObj.get("movieId");
// 2. 영화 삭제 - 해당 ID
MovieDeleteRequest movieDeleteRequest = new MovieDeleteRequest(movieId);
mvc.perform(delete(MOVIES_URL)
.content(mapper.writeValueAsString(movieDeleteRequest))
.contentType(APPLICATION_JSON))
.andExpect(status().isOk());
// then - 삭제한 영화랑 같은 제목의 영화 등록이 성공하는지 확인
mvc.perform(post(MOVIES_URL)
.content(mapper.writeValueAsString(request))
.contentType(APPLICATION_JSON))
.andExpect(status().isOk());
}
}

View File

@@ -1,5 +1,7 @@
package com.ticketing.server.user.application;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
@@ -9,8 +11,11 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ticketing.server.global.redis.RefreshRedisRepository;
import com.ticketing.server.user.application.request.LoginRequest;
import com.ticketing.server.user.application.request.RefreshRequest;
import com.ticketing.server.user.application.request.SignUpRequest;
import com.ticketing.server.user.application.response.TokenResponse;
import com.ticketing.server.user.service.interfaces.UserService;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
@@ -30,8 +35,12 @@ import org.springframework.web.context.WebApplicationContext;
class AuthControllerTest {
private static final String LOGIN_URL = "/api/auth/token";
private static final String REFRESH_URL = "/api/auth/refresh";
private static final String LOGOUT_URL = "/api/auth/logout";
private static final String REGISTER_URL = "/api/users";
private static final String USER_EMAIL = "ticketing@gmail.com";
private static final String USER_PW = "qwe123";
@Autowired
WebApplicationContext context;
@@ -54,7 +63,7 @@ class AuthControllerTest {
@DisplayName("로그인 인증 성공")
void loginSuccess() throws Exception {
// given
LoginRequest request = new LoginRequest(USER_EMAIL, "qwe123");
LoginRequest request = new LoginRequest(USER_EMAIL, USER_PW);
// when
ResultActions actions = mvc.perform(post(LOGIN_URL)
@@ -82,6 +91,69 @@ class AuthControllerTest {
.andExpect(status().isUnauthorized());
}
@Test
@DisplayName("리프레쉬 토큰 발급 성공")
void refreshTokenSuccess() throws Exception {
// given
LoginRequest loginRequest = new LoginRequest(USER_EMAIL, USER_PW);
// when
// 로그인
String loginResponseBody = mvc.perform(post(LOGIN_URL)
.content(asJsonString(loginRequest))
.contentType(MediaType.APPLICATION_JSON))
.andReturn()
.getResponse()
.getContentAsString();
TokenResponse loginResponse = objectMapper.readValue(loginResponseBody, TokenResponse.class);
RefreshRequest refreshRequest = new RefreshRequest(loginResponse.getRefreshToken());
// 토큰재발급
String refreshResponseBody = mvc.perform(post(REFRESH_URL)
.content(asJsonString(refreshRequest))
.contentType(MediaType.APPLICATION_JSON))
.andReturn()
.getResponse()
.getContentAsString();
TokenResponse refreshBody = objectMapper.readValue(refreshResponseBody, TokenResponse.class);
// then
assertAll(
() -> assertThat(refreshBody.getAccessToken()).isNotEmpty(),
() -> assertThat(refreshBody.getRefreshToken()).isNotEmpty(),
() -> assertThat(loginResponse.getTokenType()).isEqualTo(refreshBody.getTokenType()),
() -> assertThat(loginResponse.getExpiresIn()).isEqualTo(refreshBody.getExpiresIn())
);
}
@Test
@DisplayName("로그아웃 성공")
void logoutSuccess() throws Exception {
// given
LoginRequest loginRequest = new LoginRequest(USER_EMAIL, USER_PW);
// 로그인
String loginResponseBody = mvc.perform(post(LOGIN_URL)
.content(asJsonString(loginRequest))
.contentType(MediaType.APPLICATION_JSON))
.andReturn()
.getResponse()
.getContentAsString();
TokenResponse loginResponse = objectMapper.readValue(loginResponseBody, TokenResponse.class);
String authorization = loginResponse.getTokenType() + " " + loginResponse.getAccessToken();
// 로그아웃
ResultActions actions = mvc.perform(post(LOGOUT_URL)
.header("Authorization", authorization));
// then
actions.andDo(print())
.andExpect(status().isOk());
}
@BeforeEach
void init() throws Exception {
mvc = MockMvcBuilders
@@ -89,7 +161,7 @@ class AuthControllerTest {
.apply(springSecurity())
.build();
SignUpRequest signUpRequest = new SignUpRequest("ticketing", USER_EMAIL, "qwe123", "010-1234-5678");
SignUpRequest signUpRequest = new SignUpRequest("ticketing", USER_EMAIL, USER_PW, "010-1234-5678");
mvc.perform(post(REGISTER_URL)
.content(asJsonString(signUpRequest))
@@ -102,6 +174,7 @@ class AuthControllerTest {
}
private String asJsonString(Object object) throws JsonProcessingException {
return objectMapper.writeValueAsString(object);
}

View File

@@ -1,7 +1,241 @@
package com.ticketing.server.user.application;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ticketing.server.global.config.WithAuthUser;
import com.ticketing.server.user.application.request.LoginRequest;
import com.ticketing.server.user.application.request.SignUpRequest;
import com.ticketing.server.user.application.request.UserChangeGradeRequest;
import com.ticketing.server.user.application.request.UserChangePasswordRequest;
import com.ticketing.server.user.application.request.UserDeleteRequest;
import com.ticketing.server.user.domain.UserGrade;
import com.ticketing.server.user.domain.UserGrade.ROLES;
import com.ticketing.server.user.domain.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Transactional
class UserControllerTest {
private static final String LOGIN_URL = "/api/auth/token";
private static final String BASICS_URL = "/api/users";
private static final String DETAILS_URL = "/api/users/details";
private static final String CHANGE_PASSWORD_URL = "/api/users/password";
private static final String CHANGE_GRADE_URL = "/api/users/grade";
private static final String NAME = "$.name";
private static final String EMAIL = "$.email";
private static final String GRADE = "$.grade";
private static final String PHONE = "$.phone";
private static final String BEFORE_GRADE = "$.beforeGrade";
private static final String AFTER_GRADE = "$.afterGrade";
private static final String USER_EMAIL = "testemail@ticketing.com";
private static final String USER_PW = "qwe123";
private static final String USER_NAME = "김철수";
private static final String USER_PHONE = "010-1234-5678";
@Autowired
UserRepository userRepository;
@Autowired
ObjectMapper mapper;
@Autowired
WebApplicationContext context;
MockMvc mvc;
SignUpRequest signUpRequest;
@Test
@DisplayName("회원가입 성공")
void registerSuccess() throws Exception {
// given
// when
ResultActions resultActions = mvc.perform(
post(BASICS_URL)
.content(mapper.writeValueAsString(signUpRequest))
.contentType(APPLICATION_JSON)
);
// then
resultActions
.andExpect(status().isCreated())
.andExpect(content().contentType(APPLICATION_JSON))
.andExpect(jsonPath(NAME).value(USER_NAME))
.andExpect(jsonPath(EMAIL).value(USER_EMAIL));
}
@Test
@DisplayName("유저 정보 조회")
@WithAuthUser(email = USER_EMAIL, role = ROLES.USER)
void detailsSuccess() throws Exception {
// given
mvc.perform(
post(BASICS_URL)
.content(mapper.writeValueAsString(signUpRequest))
.contentType(APPLICATION_JSON)
);
// when
ResultActions resultActions = mvc.perform(
get(DETAILS_URL)
.content(mapper.writeValueAsString(signUpRequest))
.contentType(APPLICATION_JSON)
);
// then
resultActions
.andExpect(status().isOk())
.andExpect(content().contentType(APPLICATION_JSON))
.andExpect(jsonPath(NAME).value(USER_NAME))
.andExpect(jsonPath(EMAIL).value(USER_EMAIL))
.andExpect(jsonPath(GRADE).value(UserGrade.USER.name()))
.andExpect(jsonPath(PHONE).value(USER_PHONE));
}
@Test
@DisplayName("유저 탈퇴 성공")
@WithAuthUser(email = USER_EMAIL, role = ROLES.USER)
void deleteUserSuccess() throws Exception {
// given
UserDeleteRequest deleteRequest = new UserDeleteRequest(USER_EMAIL, USER_PW);
LoginRequest loginRequest = new LoginRequest(USER_EMAIL, USER_PW);
mvc.perform(
post(BASICS_URL)
.content(mapper.writeValueAsString(signUpRequest))
.contentType(APPLICATION_JSON)
);
// when
// 1. 회원 탈퇴 진행
mvc.perform(
delete(BASICS_URL)
.content(mapper.writeValueAsString(deleteRequest))
.contentType(APPLICATION_JSON)
);
// 2. 탈퇴된 계정 로그인
ResultActions resultActions = mvc.perform(post(LOGIN_URL)
.content(mapper.writeValueAsString(loginRequest))
.contentType(MediaType.APPLICATION_JSON));
// then
resultActions
.andExpect(status().isUnauthorized());
}
@Test
@DisplayName("비밀번호 변경 성공")
@WithAuthUser(email = USER_EMAIL, role = ROLES.USER)
void changePasswordSuccess() throws Exception {
// given
UserChangePasswordRequest changePasswordRequest = new UserChangePasswordRequest(USER_PW, "qwe1234");
LoginRequest loginRequest = new LoginRequest(USER_EMAIL, USER_PW);
mvc.perform(
post(BASICS_URL)
.content(mapper.writeValueAsString(this.signUpRequest))
.contentType(APPLICATION_JSON)
);
// when
// 1. 패스워드 변경
mvc.perform(
put(CHANGE_PASSWORD_URL)
.content(mapper.writeValueAsString(changePasswordRequest))
.contentType(APPLICATION_JSON)
)
.andExpect(status().isOk());
// 2. 변경 전 계정으로 로그인
ResultActions resultActions = mvc.perform(post(LOGIN_URL)
.content(mapper.writeValueAsString(loginRequest))
.contentType(MediaType.APPLICATION_JSON));
// then
resultActions
.andExpect(status().isUnauthorized());
}
@Test
@DisplayName("유저 등급 변경")
@WithAuthUser(email = "admin@ticketing.com", role = ROLES.ADMIN)
void changeGradeSuccess() throws Exception {
// given
UserChangeGradeRequest changeGradeRequest = new UserChangeGradeRequest(USER_EMAIL, UserGrade.STAFF);
mvc.perform(
post(BASICS_URL)
.content(mapper.writeValueAsString(signUpRequest))
.contentType(APPLICATION_JSON)
);
// when
ResultActions resultActions = mvc.perform(
post(CHANGE_GRADE_URL)
.content(mapper.writeValueAsString(changeGradeRequest))
.contentType(APPLICATION_JSON)
);
// then
resultActions
.andExpect(status().isOk())
.andExpect(content().contentType(APPLICATION_JSON))
.andExpect(jsonPath(EMAIL).value(USER_EMAIL))
.andExpect(jsonPath(BEFORE_GRADE).value(UserGrade.USER.name()))
.andExpect(jsonPath(AFTER_GRADE).value(UserGrade.STAFF.name()));
}
@Test
@DisplayName("유저 등급 변경 실패 - 권한 등급이 낮을 경우")
@WithAuthUser(email = "staff@ticketing.com", role = ROLES.STAFF)
void changeGradeFail() throws Exception {
// given
UserChangeGradeRequest changeGradeRequest = new UserChangeGradeRequest(USER_EMAIL, UserGrade.STAFF);
// when
ResultActions resultActions = mvc.perform(
post(CHANGE_GRADE_URL)
.content(mapper.writeValueAsString(changeGradeRequest))
.contentType(APPLICATION_JSON)
);
// then
resultActions
.andExpect(status().isForbidden());
}
@BeforeEach
void init() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
signUpRequest = new SignUpRequest(USER_NAME, USER_EMAIL, USER_PW, USER_PHONE);
}
}

View File

@@ -20,6 +20,9 @@ spring:
pathmatch:
matching-strategy: ant_path_matcher
config:
import: "optional:configserver:"
jasypt:
encryptor:
bean: jasyptStringEncryptor

View File

@@ -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) {

View File

@@ -1,4 +1,4 @@
package com.ticketing.server.user.domain;
package com.ticketing.server.global.dto;
public interface SequenceGenerator {

View File

@@ -1,4 +1,4 @@
package com.ticketing.server.user.domain;
package com.ticketing.server.global.dto;
import java.net.NetworkInterface;
import java.security.SecureRandom;

View File

@@ -17,6 +17,17 @@ public enum ErrorCode {
MISMATCH_PASSWORD(BAD_REQUEST, "비밀번호가 일치하지 않습니다."),
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, "취소할 티켓이 존재하지 않습니다."),
BAD_REQUEST_TICKET_RESERVATION(BAD_REQUEST, "이미 다른 고객이 예약 진행 중인 좌석이 존재합니다."),
BAD_REQUEST_TICKET_SOLD(BAD_REQUEST, "이미 환불 진행 중 입니다."),
NOT_REFUNDABLE_TIME(BAD_REQUEST, "환불이 가능한 시간이 지났습니다."),
NOT_REFUNDABLE_SEAT(BAD_REQUEST, "환불할 수 있는 좌석이 아닙니다."),
EMPTY_TICKET_ID(BAD_REQUEST, "티켓 정보가 존재하지 않습니다."),
/* 403 FORBIDDEN : 접근 권한 제한 */
VALID_USER_ID(FORBIDDEN, "해당 정보에 접근 권한이 존재하지 않습니다."),
@@ -25,14 +36,18 @@ public enum ErrorCode {
USER_NOT_FOUND(NOT_FOUND, "해당 유저 정보를 찾을 수 없습니다."),
EMAIL_NOT_FOUND(NOT_FOUND, "해당 이메일을 찾을 수 없습니다."),
MOVIE_NOT_FOUND(NOT_FOUND, "해당 제목의 영화를 찾을 수 없습니다."),
MOVIE_TIME_NOT_FOUND(NOT_FOUND, "해당 영화 시간표 정보를 찾을 수 없습니다"),
REFRESH_TOKEN_NOT_FOUND(NOT_FOUND, "리프레쉬 토큰을 찾을 수 없습니다."),
PAYMENT_ID_NOT_FOUND(NOT_FOUND, "결제정보를 찾을 수 없습니다."),
THEATER_NOT_FOUND(NOT_FOUND, "상영관 정보를 찾을 수 없습니다."),
/* 409 CONFLICT : Resource 의 현재 상태와 충돌. 보통 중복된 데이터 존재 */
DUPLICATE_EMAIL(CONFLICT, "이메일이 이미 존재합니다."),
DUPLICATE_PAYMENT(CONFLICT, "해당 좌석은 현재 판매된 좌석입니다."),
DUPLICATE_MOVIE(CONFLICT, "해당 영화 정보가 이미 존재합니다."),
DELETED_EMAIL(CONFLICT, "이미 삭제된 이메일 입니다.");
DUPLICATE_MOVIE_TIME(CONFLICT, "해당 영화 시간표 정보가 이미 존재합니다."),
DELETED_EMAIL(CONFLICT, "이미 삭제된 이메일 입니다."),
DELETED_MOVIE(CONFLICT, "이미 삭제된 영화 입니다.");
private final HttpStatus httpStatus;
private final String detail;
@@ -50,15 +65,24 @@ public enum ErrorCode {
throw new TicketingException(UNAVAILABLE_REFRESH_TOKEN);
}
public static TicketingException throwUnableChangeGrade() {
throw new TicketingException(UNABLE_CHANGE_GRADE);
}
public static TicketingException throwBadRequestPaymentComplete() {
throw new TicketingException(BAD_REQUEST_PAYMENT_COMPLETE);
}
public static TicketingException throwBadRequestPaymentReady() {
throw new TicketingException(BAD_REQUEST_PAYMENT_READY);
}
/* 403 FORBIDDEN : 접근 권한 제한 */
public static TicketingException throwValidUserId() {
throw new TicketingException(VALID_USER_ID);
}
/* 404 NOT_FOUND : Resource 를 찾을 수 없음 */
public static TicketingException throwUserNotFound() {
throw new TicketingException(USER_NOT_FOUND);
}
public static TicketingException throwEmailNotFound() {
throw new TicketingException(EMAIL_NOT_FOUND);
@@ -76,6 +100,10 @@ public enum ErrorCode {
throw new TicketingException(PAYMENT_ID_NOT_FOUND);
}
public static TicketingException throwTheaterNotFound() {
throw new TicketingException(THEATER_NOT_FOUND);
}
/* 409 CONFLICT : Resource 의 현재 상태와 충돌. 보통 중복된 데이터 존재 */
public static TicketingException throwDuplicateEmail() {
throw new TicketingException(DUPLICATE_EMAIL);
@@ -89,8 +117,17 @@ public enum ErrorCode {
throw new TicketingException(DUPLICATE_MOVIE);
}
public static TicketingException throwDuplicateMovieTime() {
throw new TicketingException(DUPLICATE_MOVIE_TIME);
}
public static TicketingException throwDeletedEmail() {
throw new TicketingException(DELETED_EMAIL);
}
public static TicketingException throwDeletedMovie() {
throw new TicketingException(DELETED_MOVIE);
}
}

View File

@@ -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;
@@ -22,9 +21,9 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
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;
@@ -213,6 +212,17 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
return ResponseEntity.status(response.getStatus()).headers(new HttpHeaders()).body(response);
}
/**
* 이메일이 존재하지 않을 경우
*/
@ExceptionHandler(value = BadCredentialsException.class)
protected ResponseEntity<ErrorResponse> handleBadCredentialsException(BadCredentialsException ex) {
log.error("BadCredentialsException :: ", ex);
ErrorResponse response = new ErrorResponse(UNAUTHORIZED, ex.getLocalizedMessage(), "아이디 혹은 패스워드가 일치하지 않습니다.");
return ResponseEntity.status(response.getStatus()).headers(new HttpHeaders()).body(response);
}
/**
* 인증 정보가 없을 때
*/

View File

@@ -0,0 +1,47 @@
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<Long> ticketIds;
private Long userAlternateId;
private Long paymentNumber;
private Integer totalAmount;
public PaymentCache(String email, String movieTitle, String tid, List<Long> ticketIds, Long userAlternateId, Long paymentNumber, Integer totalAmount) {
this.email = email;
this.movieTitle = movieTitle;
this.tid = tid;
this.ticketIds = ticketIds;
this.userAlternateId = userAlternateId;
this.paymentNumber = paymentNumber;
this.totalAmount = totalAmount;
}
}

View File

@@ -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<PaymentCache, Long> {
Optional<PaymentCache> findByEmail(String email);
}

View File

@@ -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();
}
}

View File

@@ -28,8 +28,9 @@ public class RefreshToken {
this.token = token;
}
public void changeToken(String token) {
public RefreshToken changeToken(String token) {
this.token = token;
return this;
}
}

View File

@@ -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()

View File

@@ -1,6 +1,6 @@
package com.ticketing.server.global.security.jwt;
import com.ticketing.server.user.application.response.TokenDto;
import com.ticketing.server.user.service.dto.TokenDTO;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
@@ -27,6 +27,7 @@ public class JwtProvider {
private static final String AUTHORITIES_KEY = "auth";
private static final String AUTHORITIES_DELIMITER = ",";
private static final String ROLE = "ROLE_";
private final Key key;
private final String prefix;
@@ -42,12 +43,12 @@ public class JwtProvider {
this.refreshTokenValidityInMilliseconds = jwtProperties.getRefreshTokenValidityInSeconds() * 1000L;
}
public TokenDto generateTokenDto(Authentication authentication) {
public TokenDTO generateTokenDto(Authentication authentication) {
String accessToken = createAccessToken(authentication);
String refreshToken = createRefreshToken(authentication);
long expiresIn = accessTokenValidityInMilliseconds / 1000L;
return TokenDto.of(accessToken, refreshToken, prefix, expiresIn);
return new TokenDTO(accessToken, refreshToken, prefix, expiresIn);
}
private String createAccessToken(Authentication authentication) {
@@ -89,7 +90,7 @@ public class JwtProvider {
}
private String makeRoleName(String role) {
return "ROLE_" + role.toUpperCase();
return role.contains(ROLE) ? role.toUpperCase() : ROLE + role.toUpperCase();
}
public Authentication getAuthentication(String token) {

View File

@@ -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<? extends Payload>[] payload() default {};
}

View File

@@ -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<NotEmptyCollection, Collection<Long>> {
@Override
public boolean isValid(Collection<Long> objects, ConstraintValidatorContext context) {
if (objects.isEmpty()) {
return false;
}
return objects.stream().allMatch(Objects::nonNull);
}
}

View File

@@ -0,0 +1,64 @@
package com.ticketing.server.movie.aop;
import static com.ticketing.server.movie.domain.TicketLock.LOCK_VALUE;
import com.ticketing.server.global.exception.ErrorCode;
import com.ticketing.server.global.exception.TicketingException;
import com.ticketing.server.movie.service.dto.TicketIdsDTO;
import java.util.List;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
@Component
@Aspect
@RequiredArgsConstructor
public class TicketLockAspect {
private final RedisTemplate<String, Object> redisTemplate;
@Around("execution(* com.ticketing.server.movie.service.TicketLockService.*(..))")
public Object ticketLock(ProceedingJoinPoint joinPoint) throws Throwable {
List<String> ticketLockIds = getTicketLockIds(joinPoint);
try {
if (!isEveryTicketIdLock(ticketLockIds)) {
throw new TicketingException(ErrorCode.BAD_REQUEST_TICKET_SOLD);
}
return joinPoint.proceed();
} finally {
ticketIdsUnlock(ticketLockIds);
}
}
protected boolean isEveryTicketIdLock(List<String> ids) {
for (String id : ids) {
if (Boolean.FALSE.equals(redisTemplate.opsForValue().setIfAbsent(id, LOCK_VALUE.getValue(), 5, TimeUnit.MINUTES))) {
return Boolean.FALSE;
}
}
return Boolean.TRUE;
}
protected Long ticketIdsUnlock(List<String> ids) {
return redisTemplate.delete(ids);
}
private List<String> getTicketLockIds(ProceedingJoinPoint joinPoint) {
for (Object arg : joinPoint.getArgs()) {
if (arg instanceof TicketIdsDTO) {
TicketIdsDTO ids = (TicketIdsDTO) arg;
return ids.makeTicketLockIds();
}
}
throw new TicketingException(ErrorCode.EMPTY_TICKET_ID);
}
}

View File

@@ -1,17 +1,26 @@
package com.ticketing.server.movie.application;
import static com.ticketing.server.user.domain.UserGrade.ROLES.STAFF;
import com.ticketing.server.movie.application.request.MovieDeleteRequest;
import com.ticketing.server.movie.application.request.MovieRegisterRequest;
import com.ticketing.server.movie.application.response.MovieDeleteResponse;
import com.ticketing.server.movie.application.response.MovieListResponse;
import com.ticketing.server.movie.application.response.MovieTitleResponse;
import com.ticketing.server.movie.application.response.MovieInfoResponse;
import com.ticketing.server.movie.service.dto.DeletedMovieDTO;
import com.ticketing.server.movie.service.dto.MovieDTO;
import com.ticketing.server.movie.service.dto.RegisteredMovieDTO;
import com.ticketing.server.movie.service.interfaces.MovieService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import java.util.List;
import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -29,25 +38,37 @@ public class MovieController {
@PostMapping()
@ApiOperation(value = "영화 정보 등록")
@Secured("ROLE_STAFF")
public ResponseEntity<MovieTitleResponse> registerMovie(@RequestBody @Valid MovieRegisterRequest request) {
@Secured(STAFF)
public ResponseEntity<MovieInfoResponse> registerMovie(@RequestBody @Valid MovieRegisterRequest request) {
RegisteredMovieDTO registeredMovieDto =
movieService.registerMovie(request.getTitle(), request.getRunningTime());
return ResponseEntity.status(HttpStatus.OK)
.body(
MovieTitleResponse.from(
movieService.registerMovie(request.toMovieRegisterDTO())
)
registeredMovieDto.toResponse()
);
}
@GetMapping()
@ApiOperation(value = "영화 목록 조회")
public ResponseEntity<MovieListResponse> getMovies() {
List<MovieDTO> movieDtos = movieService.getMovies();
return ResponseEntity.status(HttpStatus.OK)
.body(
MovieListResponse.from(
movieService.getMovies()
)
new MovieListResponse(movieDtos)
);
}
@DeleteMapping()
@ApiOperation(value = "영화 정보 삭제")
@Secured(STAFF)
public ResponseEntity<MovieDeleteResponse> deleteMovie(@RequestBody @Valid MovieDeleteRequest request) {
DeletedMovieDTO deletedMovieDto = movieService.deleteMovie(request.getId());
return ResponseEntity.status(HttpStatus.OK)
.body(
deletedMovieDto.toResponse()
);
}
}

View File

@@ -1,18 +1,30 @@
package com.ticketing.server.movie.application;
import static com.ticketing.server.user.domain.UserGrade.ROLES.STAFF;
import com.ticketing.server.movie.application.request.MovieTimeRegisterRequest;
import com.ticketing.server.movie.application.response.MovieTimeInfoResponse;
import com.ticketing.server.movie.application.response.MovieTimeListResponse;
import com.ticketing.server.movie.service.dto.MovieTimeDTO;
import com.ticketing.server.movie.service.dto.RegisteredMovieTimeDTO;
import com.ticketing.server.movie.service.interfaces.MovieTimeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import java.time.LocalDate;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.annotation.Secured;
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;
@@ -24,16 +36,35 @@ import org.springframework.web.bind.annotation.RestController;
@Slf4j
public class MovieTimeController {
private final MovieTimeService movieTimeService;
private final MovieTimeService movieTimeService;
@GetMapping
@ApiOperation(value = "영화 시간표 조회")
@Validated
public ResponseEntity<MovieTimeListResponse> getMovieTimes(
@ApiParam(value = "영화 제목", required = true) @RequestParam String title,
@ApiParam(value = "상영 날짜", required = true) @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate runningDate) {
return ResponseEntity.status(HttpStatus.OK).body(MovieTimeListResponse.from(movieTimeService.getMovieTimes(title, runningDate)));
}
@PostMapping
@ApiOperation(value = "영화 시간표 등록")
@Secured(STAFF)
public ResponseEntity<MovieTimeInfoResponse> registerMovieTime(
@RequestBody @Valid MovieTimeRegisterRequest movieTimeRegisterRequest) {
RegisteredMovieTimeDTO registeredMovieTimeDto = movieTimeService.registerMovieTime(
movieTimeRegisterRequest.toMovieTimeRegisterDTO()
);
return ResponseEntity.status(HttpStatus.OK)
.body(
registeredMovieTimeDto.toResponse()
);
}
@GetMapping
@ApiOperation(value = "영화 시간표 조회")
@Validated
public ResponseEntity<MovieTimeListResponse> getMovieTimes(
@ApiParam(value = "영화 ID", required = true) @RequestParam @NotNull Long movieId,
@ApiParam(value = "상영 날짜", required = true) @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate runningDate) {
List<MovieTimeDTO> movieTimeDtos = movieTimeService.getMovieTimes(movieId, runningDate);
return ResponseEntity.status(HttpStatus.OK)
.body(
new MovieTimeListResponse(movieTimeDtos)
);
}
}

View File

@@ -1,16 +1,24 @@
package com.ticketing.server.movie.application;
import static com.ticketing.server.user.domain.UserGrade.ROLES.USER;
import com.ticketing.server.movie.application.response.TicketDetailsResponse;
import com.ticketing.server.movie.service.dto.TicketDetailsDTO;
import com.ticketing.server.movie.application.response.TicketListResponse;
import com.ticketing.server.movie.service.dto.TicketDTO;
import com.ticketing.server.movie.service.interfaces.TicketService;
import com.ticketing.server.payment.service.dto.TicketDetailDTO;
import io.swagger.annotations.ApiParam;
import java.util.List;
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.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@@ -21,10 +29,28 @@ public class TicketController {
private final TicketService ticketService;
@GetMapping("payments/{paymentId}")
public ResponseEntity<TicketDetailsResponse> findTicketsByPaymentId(@PathVariable("paymentId") @NotNull Long paymentId) {
TicketDetailsDTO tickets = ticketService.findTicketsByPaymentId(paymentId);
return ResponseEntity.status(HttpStatus.OK).body(tickets.toResponse());
@GetMapping
@Secured(USER)
public ResponseEntity<TicketListResponse> getTickets(
@ApiParam(value = "영화 시간표 ID", required = true) @RequestParam @NotNull Long movieTimeId) {
List<TicketDTO> tickets = ticketService.getTickets(movieTimeId);
return ResponseEntity.status(HttpStatus.OK)
.body(
new TicketListResponse(tickets)
);
}
@GetMapping("/payments/{paymentId}")
@Secured(USER)
public ResponseEntity<TicketDetailsResponse> findTicketsByPaymentId(
@PathVariable("paymentId") @NotNull Long paymentId) {
List<TicketDetailDTO> tickets = ticketService.findTicketsByPaymentId(paymentId);
return ResponseEntity.status(HttpStatus.OK)
.body(
new TicketDetailsResponse(tickets)
);
}
}

View File

@@ -0,0 +1,16 @@
package com.ticketing.server.movie.application.request;
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class MovieDeleteRequest {
@NotNull(message = "{validation.not.null.id}")
private Long id;
}

View File

@@ -5,10 +5,8 @@ import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class MovieRegisterRequest {
@@ -19,7 +17,7 @@ public class MovieRegisterRequest {
private Long runningTime;
public MovieRegisterDTO toMovieRegisterDTO() {
return new MovieRegisterDTO(this.title, this.runningTime);
return new MovieRegisterDTO(title, runningTime);
}
}

View File

@@ -0,0 +1,29 @@
package com.ticketing.server.movie.application.request;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ticketing.server.movie.service.dto.MovieTimeRegisterDTO;
import java.time.LocalDateTime;
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class MovieTimeRegisterRequest {
@NotNull(message = "{validation.not.null.movieId}")
private Long movieId;
@NotNull(message = "{validation.not.null.theaterNumber}")
private Integer theaterNumber;
@NotNull(message = "{validation.not.null.round}")
private Integer round;
private LocalDateTime startAt;
public MovieTimeRegisterDTO toMovieTimeRegisterDTO() {
return new MovieTimeRegisterDTO(movieId, theaterNumber, round, startAt);
}
}

View File

@@ -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<Long> ticketIds;
}

View File

@@ -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<Long> ticketIds;
}

View File

@@ -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<Long> ticketIds;
}

View File

@@ -0,0 +1,17 @@
package com.ticketing.server.movie.application.response;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class MovieDeleteResponse {
private final Long id;
private final String title;
private final LocalDateTime deletedAt;
}

View File

@@ -0,0 +1,17 @@
package com.ticketing.server.movie.application.response;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class MovieInfoResponse {
@ApiModelProperty(value = "영화 ID")
private Long movieId;
@ApiModelProperty(value = "영화 제목")
private String title;
}

View File

@@ -3,19 +3,14 @@ package com.ticketing.server.movie.application.response;
import com.ticketing.server.movie.service.dto.MovieDTO;
import io.swagger.annotations.ApiModelProperty;
import java.util.List;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
public class MovieListResponse {
@ApiModelProperty(value = "영화 ")
@ApiModelProperty(value = "영화 목")
private List<MovieDTO> movieDtos;
public static MovieListResponse from(List<MovieDTO> movieDtos) {
return new MovieListResponse(movieDtos);
}
}

View File

@@ -0,0 +1,28 @@
package com.ticketing.server.movie.application.response;
import io.swagger.annotations.ApiModelProperty;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class MovieTimeInfoResponse {
@ApiModelProperty(value = "영화 시간표 ID")
private Long movieTimeId;
@ApiModelProperty(value = "상영관 번호")
private Integer theaterNumber;
@ApiModelProperty(value = "회차")
private Integer round;
@ApiModelProperty(value = "시작 시간")
private LocalDateTime startAt;
@ApiModelProperty(value = "종료 시간")
private LocalDateTime endAt;
}

View File

@@ -8,14 +8,10 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
public class MovieTimeListResponse {
@ApiModelProperty(value = "영화 시간표 정보")
private List<MovieTimeDTO> movieTimeDTOS;
public static MovieTimeListResponse from(List<MovieTimeDTO> movieTimeDtos) {
return new MovieTimeListResponse(movieTimeDtos);
}
@ApiModelProperty(value = "영화 시간표 목록")
private List<MovieTimeDTO> movieTimeDtos;
}

View File

@@ -1,20 +0,0 @@
package com.ticketing.server.movie.application.response;
import com.ticketing.server.movie.service.dto.MovieDTO;
import io.swagger.annotations.ApiModelProperty;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class MovieTitleResponse {
@ApiModelProperty(value = "영화 제목")
private String title;
public static MovieTitleResponse from(MovieDTO movieDto) {
return new MovieTitleResponse(movieDto.getTitle());
}
}

View File

@@ -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<Long> ticketIds;
}

View File

@@ -1,6 +1,6 @@
package com.ticketing.server.movie.application.response;
import com.ticketing.server.payment.application.response.TicketDetailDTO;
import com.ticketing.server.payment.service.dto.TicketDetailDTO;
import java.util.List;
import lombok.Getter;

View File

@@ -0,0 +1,16 @@
package com.ticketing.server.movie.application.response;
import com.ticketing.server.movie.service.dto.TicketDTO;
import io.swagger.annotations.ApiModelProperty;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class TicketListResponse {
@ApiModelProperty(value = "티켓 목록")
private List<TicketDTO> ticketDtos;
}

View File

@@ -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<TicketReservationDTO> tickets;
public int getTicketQuantity() {
return tickets.size();
}
public int getTotalPrice() {
return tickets.stream()
.mapToInt(TicketReservationDTO::getTicketPrice)
.sum();
}
}

View File

@@ -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<TicketSoldDTO> soldDtoList;
}

View File

@@ -1,7 +1,9 @@
package com.ticketing.server.movie.domain;
import com.ticketing.server.global.dto.repository.AbstractEntity;
import javax.persistence.Column;
import com.ticketing.server.global.exception.ErrorCode;
import com.ticketing.server.movie.service.dto.MovieDTO;
import java.time.LocalDateTime;
import javax.persistence.Entity;
import javax.validation.constraints.NotNull;
import lombok.AccessLevel;
@@ -16,7 +18,6 @@ import lombok.NoArgsConstructor;
public class Movie extends AbstractEntity {
@NotNull
@Column(unique = true)
private String title;
@NotNull
@@ -28,4 +29,18 @@ public class Movie extends AbstractEntity {
this.runningTime = runningTime;
}
public Movie delete() {
if (deletedAt != null) {
throw ErrorCode.throwDeletedMovie();
}
deletedAt = LocalDateTime.now();
return this;
}
public MovieDTO toMovieDTO() {
return new MovieDTO(this.id, this.title);
}
}

View File

@@ -1,6 +1,8 @@
package com.ticketing.server.movie.domain;
import com.ticketing.server.global.dto.repository.AbstractEntity;
import com.ticketing.server.movie.service.dto.MovieTimeDTO;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@@ -72,4 +74,9 @@ public class MovieTime extends AbstractEntity {
return this.theater.getSeats();
}
public MovieTimeDTO toMovieTimeDTO() {
return new MovieTimeDTO(
this.id, this.theater.getTheaterNumber(), this.round, this.startAt, this.endAt);
}
}

View File

@@ -1,8 +1,15 @@
package com.ticketing.server.movie.domain;
import static com.ticketing.server.global.exception.ErrorCode.BAD_REQUEST_PAYMENT_CANCEL;
import static com.ticketing.server.global.exception.ErrorCode.DUPLICATE_PAYMENT;
import static com.ticketing.server.global.exception.ErrorCode.NOT_REFUNDABLE_SEAT;
import static com.ticketing.server.global.exception.ErrorCode.NOT_REFUNDABLE_TIME;
import com.ticketing.server.global.dto.repository.AbstractEntity;
import com.ticketing.server.global.exception.ErrorCode;
import com.ticketing.server.global.exception.TicketingException;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
@@ -26,7 +33,7 @@ public class Ticket extends AbstractEntity {
@NotNull
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "movie_times_id", referencedColumnName = "id", updatable = false)
@JoinColumn(name = "movie_time_id", referencedColumnName = "id", updatable = false)
private MovieTime movieTime;
private Long paymentId;
@@ -53,6 +60,58 @@ public class Ticket extends AbstractEntity {
this.status = TicketStatus.SALE;
}
public Ticket makeReservation() {
if (!TicketStatus.SALE.equals(status)) {
throw new TicketingException(DUPLICATE_PAYMENT);
}
status = TicketStatus.RESERVATION;
return this;
}
public Ticket makeSold(Long paymentId) {
if (TicketStatus.SOLD.equals(status)) {
throw new TicketingException(DUPLICATE_PAYMENT);
}
status = TicketStatus.SOLD;
this.paymentId = paymentId;
return this;
}
public Ticket cancel() {
if (!TicketStatus.RESERVATION.equals(status)) {
throw new TicketingException(BAD_REQUEST_PAYMENT_CANCEL);
}
status = TicketStatus.SALE;
paymentId = null;
return this;
}
public Ticket refund(LocalDateTime dateTime) {
long seconds = ChronoUnit.SECONDS.between(dateTime, getStartAt());
if (600L > seconds) {
throw new TicketingException(NOT_REFUNDABLE_TIME);
}
return refund();
}
public Ticket refund() {
if (!TicketStatus.SOLD.equals(status)) {
throw new TicketingException(NOT_REFUNDABLE_SEAT);
}
status = TicketStatus.SALE;
paymentId = null;
return this;
}
public Long getMovieTimeId() {
return movieTime.getId();
}
public Integer getColumn() {
return this.seat.getSeatColumn();
}
@@ -85,5 +144,4 @@ public class Ticket extends AbstractEntity {
this.paymentId = id;
}
}

View File

@@ -0,0 +1,15 @@
package com.ticketing.server.movie.domain;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum TicketLock {
LOCK_KEY("TicketLock"),
LOCK_VALUE("lock");
private final String value;
}

View File

@@ -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;
}

View File

@@ -1,7 +1,6 @@
package com.ticketing.server.movie.domain.repository;
import com.ticketing.server.movie.domain.Movie;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
@@ -13,6 +12,14 @@ public interface MovieRepository extends JpaRepository<Movie, Long> {
Optional<Movie> findByTitle(String title);
Optional<Movie> findByIdAndDeletedAtNull(Long id);
@Query(value = "SELECT m "
+ "FROM Movie m "
+ "WHERE m.title = :title "
+ "AND m.deletedAt IS NULL")
Optional<Movie> findValidMovieWithTitle(String title);
@Query(value = "SELECT * "
+ "FROM movie "
+ "WHERE deleted_at IS NULL", nativeQuery = true)

View File

@@ -2,8 +2,10 @@ package com.ticketing.server.movie.domain.repository;
import com.ticketing.server.movie.domain.Movie;
import com.ticketing.server.movie.domain.MovieTime;
import com.ticketing.server.movie.domain.Theater;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
@@ -11,11 +13,13 @@ import org.springframework.stereotype.Repository;
@Repository
public interface MovieTimeRepository extends JpaRepository<MovieTime, Long> {
@Query(value = "SELECT mt "
+ "FROM MovieTime mt "
+ "JOIN FETCH mt.movie "
+ "WHERE mt.movie = :movie "
+ "AND mt.startAt BETWEEN :startOfDay AND :endOfDay ")
List<MovieTime> findValidMovieTimes(Movie movie, LocalDateTime startOfDay, LocalDateTime endOfDay);
Optional<MovieTime> findByMovieAndTheaterAndRoundAndDeletedAtNull(Movie movie, Theater theater, Integer round);
@Query(value = "SELECT mt "
+ "FROM MovieTime mt "
+ "JOIN FETCH mt.movie "
+ "WHERE mt.movie = :movie "
+ "AND mt.startAt BETWEEN :startOfDay AND :endOfDay ")
List<MovieTime> findValidMovieTimes(Movie movie, LocalDateTime startOfDay, LocalDateTime endOfDay);
}

View File

@@ -1,5 +1,6 @@
package com.ticketing.server.movie.domain.repository;
import com.ticketing.server.movie.domain.MovieTime;
import com.ticketing.server.movie.domain.Ticket;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
@@ -11,8 +12,16 @@ import org.springframework.stereotype.Repository;
public interface TicketRepository extends JpaRepository<Ticket, Long> {
@Query(
value =
"SELECT t "
value = "SELECT t "
+ "FROM Ticket t "
+ "JOIN FETCH t.seat s "
+ "WHERE t.movieTime = :movieTime "
+ "AND t.deletedAt IS NULL"
)
List<Ticket> findValidTickets(MovieTime movieTime);
@Query(
value = "SELECT t "
+ "FROM Ticket t "
+ "JOIN FETCH t.movieTime mt "
+ "JOIN FETCH t.seat s "
@@ -21,4 +30,14 @@ public interface TicketRepository extends JpaRepository<Ticket, Long> {
)
List<Ticket> 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<Ticket> findTicketFetchJoinByTicketIds(@Param("ticketIds") List<Long> ticketIds);
}

View File

@@ -3,12 +3,14 @@ package com.ticketing.server.movie.service;
import com.ticketing.server.global.exception.ErrorCode;
import com.ticketing.server.movie.domain.Movie;
import com.ticketing.server.movie.domain.repository.MovieRepository;
import com.ticketing.server.movie.service.dto.DeletedMovieDTO;
import com.ticketing.server.movie.service.dto.MovieDTO;
import com.ticketing.server.movie.service.dto.MovieRegisterDTO;
import com.ticketing.server.movie.service.dto.RegisteredMovieDTO;
import com.ticketing.server.movie.service.interfaces.MovieService;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -21,11 +23,15 @@ public class MovieServiceImpl implements MovieService {
private final MovieRepository movieRepository;
@Override
public MovieDTO registerMovie(MovieRegisterDTO movieRegisterDto) {
Optional<Movie> movie = movieRepository.findByTitle(movieRegisterDto.getTitle());
public RegisteredMovieDTO registerMovie(String title, Long runningTime) {
Optional<Movie> movie = movieRepository.findValidMovieWithTitle(title);
if(movie.isEmpty()) {
return MovieDTO.from(movieRepository.save(movieRegisterDto.toMovie()));
Movie newMovie = movieRepository.save(
new Movie(title, runningTime)
);
return new RegisteredMovieDTO(newMovie);
}
throw ErrorCode.throwDuplicateMovie();
@@ -36,9 +42,18 @@ public class MovieServiceImpl implements MovieService {
List<Movie> movies = movieRepository.findValidMovies();
return movies.stream()
.map(MovieDTO::from)
.map(movie -> movie.toMovieDTO())
.collect(Collectors.toList());
}
@Override
@Transactional
public DeletedMovieDTO deleteMovie(Long id) {
Movie movie = movieRepository.findByIdAndDeletedAtNull(id)
.orElseThrow(ErrorCode::throwMovieNotFound);
Movie deletedMovie = movie.delete();
return new DeletedMovieDTO(deletedMovie);
}
}

View File

@@ -3,14 +3,20 @@ package com.ticketing.server.movie.service;
import com.ticketing.server.global.exception.ErrorCode;
import com.ticketing.server.movie.domain.Movie;
import com.ticketing.server.movie.domain.MovieTime;
import com.ticketing.server.movie.domain.Theater;
import com.ticketing.server.movie.domain.repository.MovieRepository;
import com.ticketing.server.movie.domain.repository.MovieTimeRepository;
import com.ticketing.server.movie.domain.repository.TheaterRepository;
import com.ticketing.server.movie.service.dto.MovieTimeDTO;
import com.ticketing.server.movie.service.dto.MovieTimeRegisterDTO;
import com.ticketing.server.movie.service.dto.RegisteredMovieTimeDTO;
import com.ticketing.server.movie.service.interfaces.MovieTimeService;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -22,12 +28,34 @@ public class MovieTimeServiceImpl implements MovieTimeService {
private final MovieRepository movieRepository;
private final TheaterRepository theaterRepository;
private final MovieTimeRepository movieTimeRepository;
@Override
public List<MovieTimeDTO> getMovieTimes(String title, LocalDate runningDate) {
Movie movie = movieRepository.findByTitle(title)
.orElseThrow(ErrorCode::throwMovieNotFound);
public RegisteredMovieTimeDTO registerMovieTime(@Valid MovieTimeRegisterDTO movieTimeRegisterDto) {
Movie movie = findMovieById(movieTimeRegisterDto.getMovieId());
Theater theater = findTheaterByNumber(movieTimeRegisterDto.getTheaterNumber());
int round = movieTimeRegisterDto.getRound();
Optional<MovieTime> movieTime =
movieTimeRepository.findByMovieAndTheaterAndRoundAndDeletedAtNull(movie, theater, round);
if (movieTime.isEmpty()) {
MovieTime newMovieTime = movieTimeRepository.save(
new MovieTime(movie, theater, round, movieTimeRegisterDto.getStartAt())
);
return new RegisteredMovieTimeDTO(newMovieTime);
}
throw ErrorCode.throwDuplicateMovieTime();
}
@Override
public List<MovieTimeDTO> getMovieTimes(Long movieId, LocalDate runningDate) {
Movie movie = findMovieById(movieId);
LocalDateTime startOfDay = runningDate.atStartOfDay().plusHours(6);
LocalDateTime endOfDay = startOfDay.plusDays(1);
@@ -35,9 +63,24 @@ public class MovieTimeServiceImpl implements MovieTimeService {
List<MovieTime> movieTimes = movieTimeRepository.findValidMovieTimes(movie, startOfDay, endOfDay);
return movieTimes.stream()
.map(MovieTimeDTO::from)
.map(movieTime -> movieTime.toMovieTimeDTO())
.collect(Collectors.toList());
}
@Override
public Movie findMovieById(Long movieId) {
Movie movie = movieRepository.findByIdAndDeletedAtNull(movieId)
.orElseThrow(ErrorCode::throwMovieNotFound);
return movie;
}
@Override
public Theater findTheaterByNumber(Integer theaterNumber) {
Theater theater = theaterRepository.findByTheaterNumber(theaterNumber)
.orElseThrow(ErrorCode::throwTheaterNotFound);
return theater;
}
}

View File

@@ -0,0 +1,77 @@
package com.ticketing.server.movie.service;
import static com.ticketing.server.global.exception.ErrorCode.BAD_REQUEST_MOVIE_TIME;
import static com.ticketing.server.global.exception.ErrorCode.INVALID_TICKET_ID;
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.TicketIdsDTO;
import com.ticketing.server.movie.service.dto.TicketReservationDTO;
import com.ticketing.server.movie.service.dto.TicketSoldDTO;
import com.ticketing.server.movie.service.dto.TicketsReservationDTO;
import com.ticketing.server.movie.service.dto.TicketsSoldDTO;
import java.util.List;
import java.util.stream.Collectors;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@Service
@RequiredArgsConstructor
@Validated
public class TicketLockService {
private final TicketRepository ticketRepository;
public TicketsReservationDTO ticketReservation(@Valid TicketIdsDTO ticketIdsDto) {
List<Ticket> tickets = getTicketsByInTicketIds(ticketIdsDto.getTicketIds());
Long firstMovieTimeId = firstMovieTimeId(tickets);
List<TicketReservationDTO> reservationDtoList = tickets.stream()
.map(Ticket::makeReservation)
.filter(ticket -> firstMovieTimeId.equals(ticket.getMovieTimeId()))
.map(TicketReservationDTO::new)
.collect(Collectors.toList());
if (tickets.size() != reservationDtoList.size()) {
throw new TicketingException(BAD_REQUEST_MOVIE_TIME);
}
return new TicketsReservationDTO(firstMovieTitle(tickets), reservationDtoList);
}
public TicketsSoldDTO ticketSold(@NotNull Long paymentId, @Valid TicketIdsDTO ticketIdsDto) {
List<Ticket> tickets = getTicketsByInTicketIds(ticketIdsDto.getTicketIds());
List<TicketSoldDTO> soldDtoList = tickets.stream()
.map(ticket -> ticket.makeSold(paymentId))
.map(TicketSoldDTO::new)
.collect(Collectors.toList());
return new TicketsSoldDTO(paymentId, soldDtoList);
}
private List<Ticket> getTicketsByInTicketIds(List<Long> ticketIds) {
List<Ticket> tickets = ticketRepository.findTicketFetchJoinByTicketIds(ticketIds);
if (tickets.size() != ticketIds.size()) {
throw new TicketingException(INVALID_TICKET_ID);
}
return tickets;
}
private Long firstMovieTimeId(List<Ticket> tickets) {
Ticket ticket = tickets.get(0);
return ticket.getMovieTimeId();
}
private String firstMovieTitle(List<Ticket> tickets) {
Ticket ticket = tickets.get(0);
return ticket.getMovieTitle();
}
}

View File

@@ -1,11 +1,25 @@
package com.ticketing.server.movie.service;
import com.ticketing.server.global.exception.ErrorCode;
import static com.ticketing.server.global.exception.ErrorCode.INVALID_TICKET_ID;
import static com.ticketing.server.global.exception.ErrorCode.MOVIE_TIME_NOT_FOUND;
import static com.ticketing.server.global.exception.ErrorCode.PAYMENT_ID_NOT_FOUND;
import com.ticketing.server.global.exception.TicketingException;
import com.ticketing.server.global.validator.constraints.NotEmptyCollection;
import com.ticketing.server.movie.domain.MovieTime;
import com.ticketing.server.movie.domain.Ticket;
import com.ticketing.server.movie.domain.repository.MovieTimeRepository;
import com.ticketing.server.movie.domain.repository.TicketRepository;
import com.ticketing.server.movie.service.dto.TicketDetailsDTO;
import com.ticketing.server.movie.service.dto.TicketDTO;
import com.ticketing.server.movie.service.dto.TicketIdsDTO;
import com.ticketing.server.movie.service.dto.TicketRefundDTO;
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.application.response.TicketDetailDTO;
import com.ticketing.server.payment.service.dto.TicketDetailDTO;
import java.util.List;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import javax.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
@@ -22,19 +36,74 @@ import org.springframework.validation.annotation.Validated;
public class TicketServiceImpl implements TicketService {
private final TicketRepository ticketRepository;
private final MovieTimeRepository movieTimeRepository;
private final TicketLockService ticketLockService;
@Override
public TicketDetailsDTO findTicketsByPaymentId(@NotNull Long paymentId) {
public List<TicketDTO> getTickets(@NotNull Long movieTimeId) {
MovieTime movieTime = movieTimeRepository.findById(movieTimeId)
.orElseThrow(() -> new TicketingException(MOVIE_TIME_NOT_FOUND));
return ticketRepository.findValidTickets(movieTime)
.stream()
.map(TicketDTO::new)
.collect(Collectors.toList());
}
@Override
public List<TicketDetailDTO> findTicketsByPaymentId(@NotNull Long paymentId) {
List<TicketDetailDTO> ticketDetails = ticketRepository.findTicketFetchJoinByPaymentId(paymentId)
.stream()
.map(TicketDetailDTO::new)
.collect(Collectors.toList());
if (ticketDetails.isEmpty()) {
throw ErrorCode.throwPaymentIdNotFound();
throw new TicketingException(PAYMENT_ID_NOT_FOUND);
}
return new TicketDetailsDTO(ticketDetails);
return ticketDetails;
}
@Override
@Transactional
public TicketsReservationDTO ticketReservation(@NotEmptyCollection List<Long> ticketIds) {
return ticketLockService.ticketReservation(new TicketIdsDTO(ticketIds));
}
@Override
@Transactional
public TicketsSoldDTO ticketSold(@NotNull Long paymentId, @NotEmptyCollection List<Long> ticketIds) {
return ticketLockService.ticketSold(paymentId, new TicketIdsDTO(ticketIds));
}
@Override
@Transactional
public TicketsCancelDTO ticketCancel(@NotEmptyCollection List<Long> ticketIds) {
List<Ticket> tickets = getTicketsByInTicketIds(ticketIds);
tickets.forEach(Ticket::cancel);
return new TicketsCancelDTO(tickets);
}
@Override
@Transactional
public List<TicketRefundDTO> ticketsRefund(@NotNull Long paymentId, UnaryOperator<Ticket> refund) {
List<Ticket> tickets = ticketRepository.findTicketFetchJoinByPaymentId(paymentId);
return tickets.stream()
.map(refund)
.map(TicketRefundDTO::new)
.collect(Collectors.toList());
}
private List<Ticket> getTicketsByInTicketIds(List<Long> ticketIds) {
List<Ticket> tickets = ticketRepository.findTicketFetchJoinByTicketIds(ticketIds);
if (tickets.size() != ticketIds.size()) {
throw new TicketingException(INVALID_TICKET_ID);
}
return tickets;
}
}

View File

@@ -0,0 +1,32 @@
package com.ticketing.server.movie.service.dto;
import com.ticketing.server.movie.application.response.MovieDeleteResponse;
import com.ticketing.server.movie.domain.Movie;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class DeletedMovieDTO {
private final Long id;
private final String title;
private final LocalDateTime deletedAt;
public DeletedMovieDTO(Movie movie) {
this(
movie.getId(),
movie.getTitle(),
movie.getDeletedAt()
);
}
public MovieDeleteResponse toResponse() {
return new MovieDeleteResponse(id, title, deletedAt);
}
}

View File

@@ -1,18 +1,14 @@
package com.ticketing.server.movie.service.dto;
import com.ticketing.server.movie.domain.Movie;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
public class MovieDTO {
private Long movieId;
private String title;
public static MovieDTO from(Movie movie) {
return new MovieDTO(movie.getTitle());
}
}

View File

@@ -21,4 +21,5 @@ public class MovieRegisterDTO {
public Movie toMovie() {
return new Movie(this.title, this.runningTime);
}
}

View File

@@ -1,13 +1,11 @@
package com.ticketing.server.movie.service.dto;
import com.ticketing.server.movie.domain.MovieTime;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
public class MovieTimeDTO {
private Long movieTimeId;
@@ -20,9 +18,4 @@ public class MovieTimeDTO {
private LocalDateTime endAt;
public static MovieTimeDTO from(MovieTime movieTime) {
return new MovieTimeDTO(movieTime.getId(), movieTime.getTheater().getTheaterNumber(),
movieTime.getRound(), movieTime.getStartAt(), movieTime.getEndAt());
}
}

View File

@@ -0,0 +1,25 @@
package com.ticketing.server.movie.service.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDateTime;
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class MovieTimeRegisterDTO {
@NotNull(message = "{validation.not.null.movieId}")
private Long movieId;
@NotNull(message = "{validation.not.null.theaterNumber}")
private Integer theaterNumber;
@NotNull(message = "{validation.not.null.round}")
private Integer round;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private LocalDateTime startAt;
}

View File

@@ -0,0 +1,28 @@
package com.ticketing.server.movie.service.dto;
import com.ticketing.server.movie.application.response.MovieInfoResponse;
import com.ticketing.server.movie.domain.Movie;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class RegisteredMovieDTO {
private final Long id;
private final String title;
public RegisteredMovieDTO(Movie movie) {
this(
movie.getId(),
movie.getTitle()
);
}
public MovieInfoResponse toResponse() {
return new MovieInfoResponse(id, title);
}
}

View File

@@ -0,0 +1,38 @@
package com.ticketing.server.movie.service.dto;
import com.ticketing.server.movie.application.response.MovieTimeInfoResponse;
import com.ticketing.server.movie.domain.MovieTime;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class RegisteredMovieTimeDTO {
private final Long movieTimeId;
private final Integer theaterNumber;
private final Integer round;
private final LocalDateTime startAt;
private final LocalDateTime endAt;
public RegisteredMovieTimeDTO(MovieTime movieTime) {
this(
movieTime.getId(),
movieTime.getTheater().getTheaterNumber(),
movieTime.getRound(),
movieTime.getStartAt(),
movieTime.getEndAt()
);
}
public MovieTimeInfoResponse toResponse() {
return new MovieTimeInfoResponse(movieTimeId, theaterNumber, round, startAt, endAt);
}
}

View File

@@ -0,0 +1,33 @@
package com.ticketing.server.movie.service.dto;
import com.ticketing.server.movie.domain.Ticket;
import com.ticketing.server.movie.domain.TicketStatus;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class TicketDTO {
private Long ticketId;
private Integer ticketPrice;
private Integer seatRow;
private Integer seatColumn;
private TicketStatus status;
public TicketDTO(Ticket ticket) {
this(
ticket.getId(),
ticket.getTicketPrice(),
ticket.getRow(),
ticket.getColumn(),
ticket.getStatus()
);
}
}

View File

@@ -1,18 +0,0 @@
package com.ticketing.server.movie.service.dto;
import com.ticketing.server.movie.application.response.TicketDetailsResponse;
import com.ticketing.server.payment.application.response.TicketDetailDTO;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class TicketDetailsDTO {
private final List<TicketDetailDTO> ticketDetails;
public TicketDetailsResponse toResponse() {
return new TicketDetailsResponse(ticketDetails);
}
}

View File

@@ -0,0 +1,24 @@
package com.ticketing.server.movie.service.dto;
import static com.ticketing.server.movie.domain.TicketLock.LOCK_KEY;
import com.ticketing.server.global.validator.constraints.NotEmptyCollection;
import java.util.List;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class TicketIdsDTO {
@NotEmptyCollection
private List<Long> ticketIds;
public List<String> makeTicketLockIds() {
return ticketIds.stream()
.map(id -> LOCK_KEY.getValue() + ":" + id)
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,20 @@
package com.ticketing.server.movie.service.dto;
import com.ticketing.server.movie.domain.Ticket;
import com.ticketing.server.movie.domain.TicketStatus;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class TicketRefundDTO {
private Long ticketId;
private TicketStatus ticketStatus;
public TicketRefundDTO(Ticket ticket) {
this(ticket.getId(), ticket.getStatus());
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -0,0 +1,24 @@
package com.ticketing.server.movie.service.dto;
import com.ticketing.server.movie.application.response.TicketCancelResponse;
import com.ticketing.server.movie.domain.Ticket;
import java.util.List;
import java.util.stream.Collectors;
import lombok.Getter;
@Getter
public class TicketsCancelDTO {
private final List<Long> ticketIds;
public TicketsCancelDTO(List<Ticket> tickets) {
ticketIds = tickets.stream()
.map(Ticket::getId)
.collect(Collectors.toList());
}
public TicketCancelResponse toResponse() {
return new TicketCancelResponse(ticketIds);
}
}

View File

@@ -0,0 +1,21 @@
package com.ticketing.server.movie.service.dto;
import java.util.List;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class TicketsRefundResponse {
private final List<TicketRefundDTO> tickets;
public List<Long> getTicketIds() {
return tickets.stream()
.mapToLong(TicketRefundDTO::getTicketId)
.boxed()
.collect(Collectors.toList());
}
}

View File

@@ -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<TicketReservationDTO> ticketReservationDtoList;
public TicketReservationResponse toResponse() {
return new TicketReservationResponse(movieTitle, ticketReservationDtoList);
}
}

View File

@@ -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<TicketSoldDTO> soldDtoList;
public TicketSoldResponse toResponse() {
return new TicketSoldResponse(paymentId, soldDtoList);
}
}

View File

@@ -1,13 +1,15 @@
package com.ticketing.server.movie.service.interfaces;
import com.ticketing.server.movie.service.dto.DeletedMovieDTO;
import com.ticketing.server.movie.service.dto.MovieDTO;
import com.ticketing.server.movie.service.dto.MovieRegisterDTO;
import com.ticketing.server.movie.service.dto.RegisteredMovieDTO;
import java.util.List;
public interface MovieService {
MovieDTO registerMovie(MovieRegisterDTO movieRegisterDto);
RegisteredMovieDTO registerMovie(String title, Long runningTime);
List<MovieDTO> getMovies();
List<MovieDTO> getMovies();
DeletedMovieDTO deleteMovie(Long id);
}

View File

@@ -1,11 +1,22 @@
package com.ticketing.server.movie.service.interfaces;
import com.ticketing.server.movie.domain.Movie;
import com.ticketing.server.movie.domain.Theater;
import com.ticketing.server.movie.service.dto.MovieTimeDTO;
import com.ticketing.server.movie.service.dto.MovieTimeRegisterDTO;
import com.ticketing.server.movie.service.dto.RegisteredMovieTimeDTO;
import java.time.LocalDate;
import java.util.List;
import javax.validation.Valid;
public interface MovieTimeService {
List<MovieTimeDTO> getMovieTimes(String title, LocalDate runningDate);
RegisteredMovieTimeDTO registerMovieTime(@Valid MovieTimeRegisterDTO movieTimeRegisterDto);
List<MovieTimeDTO> getMovieTimes(Long movieId, LocalDate runningDate);
Movie findMovieById(Long movieId);
Theater findTheaterByNumber(Integer theaterNumber);
}

View File

@@ -1,10 +1,31 @@
package com.ticketing.server.movie.service.interfaces;
import com.ticketing.server.movie.service.dto.TicketDetailsDTO;
import com.ticketing.server.global.validator.constraints.NotEmptyCollection;
import com.ticketing.server.movie.domain.Ticket;
import com.ticketing.server.movie.service.dto.TicketDTO;
import com.ticketing.server.movie.service.dto.TicketIdsDTO;
import com.ticketing.server.movie.service.dto.TicketRefundDTO;
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.payment.service.dto.TicketDetailDTO;
import java.util.List;
import java.util.function.UnaryOperator;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
public interface TicketService {
TicketDetailsDTO findTicketsByPaymentId(@NotNull Long paymentId);
List<TicketDTO> getTickets(@NotNull Long movieTimeId);
List<TicketDetailDTO> findTicketsByPaymentId(@NotNull Long paymentId);
TicketsReservationDTO ticketReservation(@NotEmptyCollection List<Long> ticketIds);
TicketsSoldDTO ticketSold(@NotNull Long paymentId, @NotEmptyCollection List<Long> ticketIds);
TicketsCancelDTO ticketCancel(@NotEmptyCollection List<Long> ticketIds);
List<TicketRefundDTO> ticketsRefund(@NotNull Long paymentId, UnaryOperator<Ticket> refund);
}

View File

@@ -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;
@@ -46,7 +46,7 @@ MovieSetupService {
initTheater();
initMovieTime();
initTicket();
initPayment();
// initPayment();
}
private void initMovie() {
@@ -119,8 +119,17 @@ MovieSetupService {
List<Ticket> 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,
30_000
);
Payment payment = new Payment(paymentCache, PaymentType.KAKAO_PAY, PaymentStatus.SOLD, 30_000);
paymentRepository.save(payment);

View File

@@ -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();
}
}

View File

@@ -0,0 +1,32 @@
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.KakaoPayCancelRequest;
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.KakaoPayCancelResponse;
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);
@PostMapping(value = "/cancel", consumes = APPLICATION_FORM_URLENCODED_VALUE)
@Headers("Content-Type: application/x-www-form-urlencoded;charset=utf-8")
KakaoPayCancelResponse cancel(@RequestHeader(value = "Authorization") String authorization, KakaoPayCancelRequest request);
}

View File

@@ -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;
}
}

View File

@@ -1,10 +1,28 @@
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.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.TicketsRefundResponse;
import com.ticketing.server.payment.api.impl.TicketsRefundRequest;
import java.time.LocalDateTime;
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);
TicketsRefundResponse ticketRefundByDateTime(TicketsRefundRequest request, LocalDateTime dateTime);
TicketsRefundResponse ticketRefund(TicketsRefundRequest request);
}

View File

@@ -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 {
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,47 @@
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 KakaoPayCancelRequest {
@NotEmpty
private String cid = "TC0ONETIME";
@feign.form.FormProperty("cid_secret")
private String cidSecret;
@NotEmpty
private String tid;
@NotEmpty
@feign.form.FormProperty("cancel_amount")
private Integer cancelAmount;
@NotEmpty
@feign.form.FormProperty("cancel_tax_free_amount")
private Integer cancelTaxFreeAmount;
@feign.form.FormProperty("cancel_vat_amount")
private Integer cancelVatAmount;
@feign.form.FormProperty("cancel_available_amount")
private Integer cancelAvailableAmount;
private String payload;
public KakaoPayCancelRequest(String tid, Integer cancelAmount) {
this(tid, cancelAmount, 0);
}
public KakaoPayCancelRequest(String tid, Integer cancelAmount, int cancelTaxFreeAmount) {
this.tid = tid;
this.cancelAmount = cancelAmount;
this.cancelTaxFreeAmount = cancelTaxFreeAmount;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,16 @@
package com.ticketing.server.payment.api.dto.response;
import com.ticketing.server.payment.api.dto.SnakeCaseStrategy;
import lombok.Getter;
@Getter
public class ApprovedCancelAmount extends SnakeCaseStrategy {
private Integer total;
private Integer taxFree;
private Integer vat;
private Integer point;
private Integer discount;
private Integer greenDeposit;
}

View File

@@ -0,0 +1,16 @@
package com.ticketing.server.payment.api.dto.response;
import com.ticketing.server.payment.api.dto.SnakeCaseStrategy;
import lombok.Getter;
@Getter
public class CancelAvailableAmount extends SnakeCaseStrategy {
private Integer total;
private Integer taxFree;
private Integer vat;
private Integer point;
private Integer discount;
private Integer greenDeposit;
}

View File

@@ -0,0 +1,16 @@
package com.ticketing.server.payment.api.dto.response;
import com.ticketing.server.payment.api.dto.SnakeCaseStrategy;
import lombok.Getter;
@Getter
public class CanceledAmount extends SnakeCaseStrategy {
private Integer total;
private Integer taxFree;
private Integer vat;
private Integer point;
private Integer discount;
private Integer greenDeposit;
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,50 @@
package com.ticketing.server.payment.api.dto.response;
import com.ticketing.server.payment.api.dto.SnakeCaseStrategy;
import java.time.LocalDateTime;
import lombok.Getter;
@Getter
public class KakaoPayCancelResponse extends SnakeCaseStrategy {
private String aid;
private String tid;
private String cid;
private String status;
private String partnerOrderId;
private String partnerUserId;
private String paymentMethodType;
private Amount amount;
private ApprovedCancelAmount approvedCancelAmount;
private CanceledAmount canceledAmount;
private CancelAvailableAmount cancelAvailableAmount;
private String itemName;
private String itemCode;
private Integer quantity;
private LocalDateTime createdAt;
private LocalDateTime approvedAt;
private LocalDateTime canceledAt;
private String payload;
public Integer getTotalAmount() {
return amount.getTotal();
}
}

View File

@@ -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;
}

View File

@@ -1,9 +1,23 @@
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.service.dto.TicketDetailsDTO;
import com.ticketing.server.movie.application.response.TicketReservationResponse;
import com.ticketing.server.movie.application.response.TicketSoldResponse;
import com.ticketing.server.movie.domain.Ticket;
import com.ticketing.server.movie.service.dto.TicketRefundDTO;
import com.ticketing.server.movie.service.dto.TicketsCancelDTO;
import com.ticketing.server.movie.service.dto.TicketsRefundResponse;
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 com.ticketing.server.payment.service.dto.TicketDetailDTO;
import java.time.LocalDateTime;
import java.util.List;
import javax.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -18,8 +32,38 @@ public class MovieClientImpl implements MovieClient {
@Override
public TicketDetailsResponse getTicketsByPaymentId(@NotNull Long paymentId) {
TicketDetailsDTO ticketDetails = ticketService.findTicketsByPaymentId(paymentId);
return ticketDetails.toResponse();
List<TicketDetailDTO> ticketDetails = ticketService.findTicketsByPaymentId(paymentId);
return new TicketDetailsResponse(ticketDetails);
}
@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();
}
@Override
public TicketsRefundResponse ticketRefundByDateTime(TicketsRefundRequest request, LocalDateTime dateTime) {
List<TicketRefundDTO> ticketRefundDTOS = ticketService.ticketsRefund(request.getPaymentId(), ticket -> ticket.refund(dateTime));
return new TicketsRefundResponse(ticketRefundDTOS);
}
@Override
public TicketsRefundResponse ticketRefund(TicketsRefundRequest request) {
List<TicketRefundDTO> ticketRefundDtos = ticketService.ticketsRefund(request.getPaymentId(), Ticket::refund);
return new TicketsRefundResponse(ticketRefundDtos);
}
}

View File

@@ -0,0 +1,12 @@
package com.ticketing.server.payment.api.impl;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class TicketsRefundRequest {
private final Long paymentId;
}

View File

@@ -1,19 +1,39 @@
package com.ticketing.server.payment.application;
import static com.ticketing.server.user.domain.UserGrade.ROLES.STAFF;
import static com.ticketing.server.user.domain.UserGrade.ROLES.USER;
import com.ticketing.server.payment.application.request.PaymentReadyRequest;
import com.ticketing.server.payment.application.request.PaymentRefundRequest;
import com.ticketing.server.payment.application.response.PaymentCancelResponse;
import com.ticketing.server.payment.application.response.PaymentCompleteResponse;
import com.ticketing.server.payment.application.response.PaymentDetailResponse;
import com.ticketing.server.payment.application.response.PaymentRefundResponse;
import com.ticketing.server.payment.application.response.SimplePaymentsResponse;
import com.ticketing.server.payment.service.AdminKakaoPayRefundService;
import com.ticketing.server.payment.service.MyKakaoPayRefundService;
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.dto.PaymentRefundDTO;
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.payment.application.response.SimplePaymentsResponse;
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
@@ -24,19 +44,72 @@ public class PaymentController {
private final PaymentApisService paymentApisService;
private final PaymentService paymentService;
private final AdminKakaoPayRefundService adminKakaoPayRefundService;
private final MyKakaoPayRefundService myKakaoPayRefundService;
@GetMapping
@Secured(UserGrade.ROLES.USER)
@Secured(USER)
public ResponseEntity<SimplePaymentsResponse> simplePayments(@NotNull Long userAlternateId) {
SimplePaymentsDTO simplePayments = paymentService.findSimplePayments(userAlternateId);
return ResponseEntity.status(HttpStatus.OK).body(simplePayments.toResponse());
return ResponseEntity.status(HttpStatus.OK)
.body(simplePayments.toResponse());
}
@GetMapping("/detail")
@Secured(UserGrade.ROLES.USER)
@Secured(USER)
public ResponseEntity<PaymentDetailResponse> detail(@NotNull Long paymentId) {
PaymentDetailResponse paymentDetail = paymentApisService.findPaymentDetail(paymentId);
return ResponseEntity.status(HttpStatus.OK).body(paymentDetail);
PaymentDetailDTO paymentDetail = paymentApisService.findPaymentDetail(paymentId);
return ResponseEntity.status(HttpStatus.OK)
.body(paymentDetail.toResponse());
}
@PostMapping("/ready")
@Secured(USER)
public ResponseEntity<PaymentReadyDTO> ready(@RequestBody @Valid PaymentReadyRequest request) {
PaymentReadyDTO paymentReadyDto = paymentApisService.ready(request.getTicketIds());
return ResponseEntity.status(HttpStatus.OK)
.body(paymentReadyDto);
}
@GetMapping("/complete")
@Secured(USER)
public ResponseEntity<PaymentCompleteResponse> 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(USER)
public ResponseEntity<PaymentCancelResponse> cancel(@AuthenticationPrincipal UserDetails userRequest) {
PaymentCancelDTO paymentCancelDto = paymentApisService.cancel(userRequest.getUsername());
return ResponseEntity.status(HttpStatus.OK)
.body(paymentCancelDto.toResponse());
}
@PostMapping("/refund")
@Secured(USER)
public ResponseEntity<PaymentRefundResponse> refund(@RequestBody @Valid PaymentRefundRequest request) {
PaymentRefundDTO paymentRefundDto = myKakaoPayRefundService.refund(request.getPaymentId());
return ResponseEntity.status(HttpStatus.OK)
.body(paymentRefundDto.toResponse());
}
@PostMapping("/staff/refund")
@Secured(STAFF)
public ResponseEntity<PaymentRefundResponse> adminRefund(@RequestBody @Valid PaymentRefundRequest request) {
PaymentRefundDTO paymentRefundDto = adminKakaoPayRefundService.refund(request.getPaymentId());
return ResponseEntity.status(HttpStatus.OK)
.body(paymentRefundDto.toResponse());
}
}

View File

@@ -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<Long> ticketIds;
}

View File

@@ -0,0 +1,12 @@
package com.ticketing.server.payment.application.request;
import javax.validation.constraints.NotNull;
import lombok.Getter;
@Getter
public class PaymentRefundRequest {
@NotNull
private Long paymentId;
}

View File

@@ -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<Long> ticketIds;
}

Some files were not shown because too many files have changed in this diff Show More