From 09b97dc3cab430ef8e422796d8451a849cba53b6 Mon Sep 17 00:00:00 2001 From: Yeongsang Jo <61370901+joelonsw@users.noreply.github.com> Date: Sun, 20 Nov 2022 11:52:27 +0900 Subject: [PATCH] deploy: v2.1.0 (#177) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 유저 정보 조회 (#1) * feat: 유저 엔티티 생성 * feat: 유저 조회 요청 구현 * feat: Spring Security 설정 * test: UserController에 대한 테스트 작성 * feat: queryDsl 적용 (#2) * feat: QueryDsl 초기 설정 * style: import 정리 * feat: pr 템플릿 구성 (#3) * feat: jwt 토큰 생성 및 클레임 추출 (#4) * feat: 유저 토큰 발행기능 * feat: 유저 토큰 검증 * feat: 토큰 데이터 속성 추출 * feat: 유저 탈퇴 기능 추가 (#5) * feat: klip 로그인 구현 (#6) * feat: klip 로그인 초기 설정 구현 * feat: klip 로그인 구현 * test: klip 로그인에 대한 UserService 테스트 작성 * feat: Cors 이슈 수정 (#7) * feat: 크로스도메인 이슈 해결 * feat: 크로스도메인 이슈 수정 * feat: AWS ParameterStore 기능 추가 (#8) * feat: AWS ParameterStore 기능 추가 * feat: Jwt 설정 추가 * fix: aws parameter-store 의존성 버전 수정 (#9) * fix: aws parameterStore 의존성 버전 수정 * fix: 활성환경 dev->local 수정 * fix: Klip 로그인만 수행한 유저에 대해 isActive 값 상이하게 지정 (#10) * fix: WebConfigure 경로 수정 (#11) * feat: spring security 초기 설정 (#12) * feat: CustomUserDetailsService 구현 * feat: JwtAuthenticationFilter 구현 * feat: spring security 구현 * refactor: ConnectableUserDetail로 user 가져오도록 수정 * feat: 유저 수정 기능 추가 (#13) * feat: 유저 수정 기능 추가 * feat: 사용하지 않는 Transactional 패키지 제거 * fix: user 도메인 npe 수정 (#14) * fix: cors methods 추가 (#15) * fix: 스프링 시큐리티 Options 설정 (#16) * fix: write connection 트랜잭셔널 어노테이션 추가 (#18) * feat: 이벤트 목록 조회 (#17) * feat: 이벤트 목록 조회 * feat: LocalDateTime->TimeStamp 변환을 Dto 내에서 하도록 수정 * feat: ticket 엔티티 구현 및 dataLoader 도입 (#19) * fix: transactional ReadOnly 속성 삭제 (#20) * feat: exception global 처리 추가 (#21) * fix: 유저 상세 조회시 status 추가 (#22) * fix: modify user transactional 선언 (#23) * feat: 유저 수정 기능 queryDsl로 변경 (#24) * feat: events detail (#25) * feat: 이벤트 상세 조회 기능 추가 * feat: Artist 도메인 추가 및 Event연관관계 매핑 * refactor: DB Trans 오브젝트 경로 변경 * feat: 이벤트 조회 api (#26) * feat: 이벤트에 속한 티켓 리스트 조회 * feat: 이벤트에 속한 티켓 상세 정보 조회 * test: 이벤트 티켓 리스트 & 티켓 상세 조회 통합 테스트 코드 * feat: 유저 소유 티켓 목록 조회 기능 추가 (#27) * fix: 프론트 요구 명세에 따라 status 필드 추가 (#28) * fix: QA 수정사항(타임스탬프 변환, artistImage필드 추가, 더미데이터 추가) (#29) * fix: 티켓 메타데이터 null로 내려오는 이슈 수정 (#30) * fix: 티켓 메타데이터 미노출 수정 * fix: 더미데이터 테스트로 테스트 케이스 깨지는 현상수정 * fix: 유저 소지한 티켓데이터 내 티켓메타데이터 필드 null 현상 수정 (#31) * fix: 티켓 메타데이터 미노출 수정 * fix: 더미데이터 테스트로 테스트 케이스 깨지는 현상수정 * fix: /users/tickets 엔드포인트 티켓 메타데이터 수정 * feat: KasService 구현 (#32) * build: KAS 설정을 위한 세팅 * feat: KasService 사용을 위한 Dto 생성 * feat: KasException 생성 * feat: KasService 생성 * build: yml 설정값 부여 * feat: DataLoader 정보 추가 * refactor: builder 어노테이션 생성자에 붙이도록 수정 * refactor: 티켓 도메인 쿼리 인덴트 수정 및 QueryProjection 적용, User metadata fix (#33) * fix: 티켓 메타데이터 dto 도입 (#34) * fix: tokenId로 KAS 접근하도록 수정 (#35) * feat: 유저 수정 request dto 검증 기능 (#37) * feat: 유저 수정 request dto 검증 기능 * fix: 닉네임 검증로직 및 문구 수정 * refactor: KAS API 수정 (#36) * refactor: dto 패키지 구조 변경 * build: kas 관련 yml 설정 변경 * fix: dataloader 설정값 변경 * fix: 유저 아티스트명 필드 누락 수정 (#38) * feat: nickname validation 도입 (#39) * refactor: onSale 필드를 TicketSalesStatus로 변경 (#41) * refactor: SalesOption을 EventSalesOption으로 수정 * refactor: onSale 필드를 TicketSalesStatus로 변경 * refactor: EventRepository NPE 발생이슈 Optional로 리팩토링 (#40) * refactor: EventRepository NPE 발생이슈 Optional로 리팩토링 * fix: HttpStatus INTENAL_SERVER_ERROR -> BAD_REQUEST 변경 * refactor: service 레이어 Mapper 사용하여 DTO 변환 및 컨트롤러 ResponseEntity 변경 (#42) * feat: api 문서화 (#43) * feat: api 문서화 * refactor: global/config 패키지로 이동 * fix: ticketSalesStatus null 로 내려오는 현상 수정 (#45) * feat: 주문 등록 기능 API 추가 (#44) * feat: 티켓 예매 폼 제출 api * refactor: 이력관리를 위한 Baseentity 추가 및 Order 테이블 예약어 이슈 * test: OrderService 테스트코드 * refactor: ManyToOne 에서 연관관계 OneToMany로 변경 * fix: 주문 상태 등록시 상태 메시지 변경 (#46) * refactor: order와 orderDetail를 양방향으로 수정 (#47) * refactor: order와 orderDetail를 양방향으로 수정 * fix: 깨지는 테스트 수정 * refactor: TicketMetadata hashcode/equals 정의 (#48) * fix: dataloader 로직 오류 수정 (#49) * fix: dataloader 로직 오류 수정 * refactor: TicketSalesStatus Expired 상태 추가 * fix: KLIP 주소를 소문자로 통일 (#51) * refactor: contractAddress 추가 (#52) * feat: 어드민을 통한 OrderDetail 상태 변경 기능 구현 (#50) * feat: OrderDetail 도메인 로직 추가 - orderStatus 상태변경 enum으로 관리 - 연관관계를 가진 엔티티의 필드 getter를 OrderDetail 자체의 getter로 추상화 * feat: Admin 엔드포인트 작성 * feat: Admin 서비스 구현 * refactor: 엔드포인트 patch 요청으로 변경 * feat: admin 토큰 검증 로직 구현 (#53) * feat(JwtProvider): 어드민 토큰 검증 기능 구현 * feat: 어드민 토큰 Filter 적용 * fix: 티켓 전송 엔티티 반영 로직 추가 (#54) * feat: TicketSalesStatus 정보 동기화 (#55) * feat: 티켓 주문시 Pending으로 상태 변경 * feat: OrderStatus에 따른 TicketSalesStatus 정보 업데이트 * refactor: KAS를 통해 사용자 티켓 정보 조회 (#57) * feat(EventRepository): contract 주소 추출 기능 구현 * refactor: KAS를 통해 사용자의 티켓을 가져오도록 수정 * fix: KasService 비동기 예외처리 * feat: ID 기반으로 엔티티 equals/hashcode 재정의 * feat: Cron job으로 판매 종료된 이벤트의 티켓들 EXPIRED 처리 (#56) * feat: 만료일자 지난 티켓에 대하여 판매 중지처리 * refactor: 사용하지 않는 패키지 제외 * fix: 스케줄러 패키지 경로 변경 * fix: scheduledTask Component 등록 * fix: dataloader 설정 재정의 (#58) * fix: dataloader 설정 재정의 * fix: TicketRepository implements 재정의 * build: yml ddl-auto 값 파라미터 스토어로 이전 * feat: 거래 내역 조회 api (#59) * feat: 거래 내역 조회 api * refactor: 불필요한 개행 제거 * fix: 쿼리 수정 및 ticketMetadata 필드 추가 * feat: dataloader에서 새로운 nft 발행기능 구현 (#60) * feat(KasService): alias로 컨트랙트 정보 가져오는 기능 구현 * feat(S3Service): tokenUri로 부터 metadata를 가져오는 기능 구현 * feat(TicketMetadataResponse): Dto에서 TicketMetadata로 변환해주는 메서드 구현 * refactor: 새로운 nft 민팅 기능 구현 * fix(S3ServiceTest): 깨지는 테스트 수정 (#61) * fix: totalTicketCount 잘못 내려오는 현상 수정 (#62) * fix: timestamp 필드 변환 수정 (#64) * fix: 주문자명 정규식 검증 제거 (#65) * feat: path, token 로깅 (#66) * feat: 티켓 및 이벤트 정렬 (#67) * fix: 티켓 정렬 순서 변경 (#68) * feat: 주문 신청시 request 로그 슬랙알림 (#69) * fix: get user 로직 수정 (#70) * refactor: 예외 처리 전략 수립 (#71) * feat: 예외 처리 전략 수립 * test: 깨지는 테스트 수정 * feat: 전역 Exception 핸들러 추가 * feat: 주문 상세 내역 필드 추가 (#72) * feat: Opensea Collection Url 추가 (#73) * refactor: 트랜잭션 어노테이션 정책에 따라 변경 및 단건 더티체킹 (#74) * refactor: 유저 변경 트랜잭션 전파 설정 변경 (#75) * fix: 유저 수정 queryDsl 로 변경 (#76) * feat: 로깅 전략 수립 (#77) * refactor: user details 수정 (#78) * feat(ConnectableUserDetails): UserDetails가 klaytnAddress를 가지고 있도록 수정 * refactor(UserService): 서비스 내에서 user를 조회하도록 수정 * refactor(OrderService): 서비스 내에서 user를 조회하도록 수정 * test: 깨지는 테스트 수정 * build: local profile console logging 추가 * feat: ConnectableSecurityException 정의 * refactor(JwtProvider): try-catch를 통한 검증로직 추가 * style(SecurityConfiguration): configure 로직별 개행 추가 * feat(JwtAuthenticationFilter): try-catch 추가 * feat: 유저 인수테스트 추가 (#79) * feat: 유저 인수테스트 추가 * fix: success 필드 직렬화되는 현상 수정 * refactor: 정책에 따라 fetchType 명시 제거 (#80) * refactor: swagger 설정 변경 (#81) * feat: swagger 설정 * refactor: ApiImplicitParam 어노테이션 제거 * feat: 전역 헤더 요청 값 추가 * fix: swagger uri 설정 (#82) * feat: prod 환경 yaml 설정 (#83) * feat: 인프라 관련 file 추가 (#84) * fix: bash 쉘 오류 확인 여부 (#85) * feat: plainjar 삭제 (#86) * fix: region 명시 (#87) * fix: region 변경 (#88) * fix: task 강제 업데이트 되도록 수정 (#89) * fix: Jenkinsfile 브랜치별 분기 (#90) * fix: Jenkinsfile 문법 수정 (#91) * fix: jenkinsfile 문법 수정 (#92) * fix: Jenkinsfile 문법 수정 (#93) * refactor: dockerfile profile 적용 (#94) * fix: prod환경 log path 수정 (#96) * feat: flyway 도입 (#99) * feat: flyway 적용 * feat(Ticket): isUsed 필드 추가 * feat: ticket user 연관관계 제거 * style: 안쓰는 import 제거 * feat: fk 네이밍 컨벤션 확립 * refactor: TicketResponse isUsed 필드 추가 (#100) * refactor: TicketResponse isUsed 필드 추가 * fix: EventMapper가 isUsed 필드 가져오도록 수정 * refactor: DB에서 ContractName을 가지도록 수정 (#101) * refactor(Event): ContractName DB에 저장하도록 수정 * refactor(EventMapper): ticketToResponse ownedBy 필드 무시하도록 수정 * feat: 입장 처리를 위한 입장 정보 생성 (#103) * feat: 입장 처리를 위한 입장 정보 생성 로직 구현 * refactor: RedisHash 값 부여 * feat: 회원가입 문자 인증 기능 (#104) * feat: 회원가입 문자 인증 기능 * fix: ResponseEntity로 응답처리 및 apiDomain static final * fix: RedisConfig 충돌해결 (#105) * feat: 티켓 입장 기능 구현 (#106) * feat: 티켓 입장 API 구현 * refactor(Ticket): useToEnter 네이밍 변경 * test: 티켓 입장 관련 테스트 작성 * fix: 티켓 사용 여부 로직에 Transactional 추가 (#107) * fix: 티켓 사용 여부 로직에 Transactional 추가 * test(UserServiceTest): 티켓 업데이트 테스트 수정 * chore: 인증 메시지 문구 수정 (#108) * feat: 컨트랙트 발행 및 토큰 민팅 기능 구현 (#110) * fix: nurigo exception추가 및 insert쿼리 작성 (#111) * fix: exception error 로그 삽입 및 insert query 추가 * fix: 안쓰는 패키지 정리 * fix: event insert query 삭제 * fix: 컨트랙트 배포 로직 수정 (#112) * fix: eventSalesOption -> salesOption (#114) * fix: event 테이블 필드명 변경 (#116) * feat: 무작위 티켓 구매 로직 구현 (#118) * fix: 유저 토큰 조회시 id와 uri 검증하도록 수정 (#120) * feature: 이벤트입장 로그 기록 (#122) * feat: spotless 적용하여 코드 포맷팅 (#123) * feat: new artist for letslock (#124) * fix: 아티스트명, 이미지 변경 (#125) * fix: 이벤트 노출 순서 변경 (#127) * fix: artistname 변경 (#129) * fix: artistname 변경 * fix: 아티스트 이미지 변경 * fix: 이미지 url 변경 * fix: 이미지 url 변경_Retry * fix: HandlerMethodArgumentResolverComposite이슈 수정 (#131) * feat: 이벤트 목록 조회 순서 변경 (#132) * feat: 이벤트 목록 조회 순서 변경 * fix: event조회 함수명 추가 및 repository 테스트 코드 추가 * refactor: KasService 제네릭 적용 및 리팩터링 (#133) * feat: KasContractService, KasTokenService 분리 * refactor(KasWebClient): Http 요청에 따른 제네릭 Mono 반환하도록 수정 * feat(KasUrlGenerator): Kas에 사용되는 URL 관리하는 유틸 클래스 생성 * test: KasService 테스트 작성 (#134) * refactor: url 생성을 인터페이스로 분리 * refactor: 유저 토큰 전체 조회 메서드 네임 변경 * build: MockServer 도입 * refactor(KasService): getMyContractByAlias로 메서드 네이밍 변경 * refactor: KasException 구성 변경 * test: KasService Mocking 서버 도입 * style: 코드 포맷팅 * fix(KasTokenService): HashMap 동시성 이슈로 ConcurrentHashMap으로 대체 * build: mockServerVersion buildscript에 명시 * refactor: KasMockConfig 도입 * refactor(KasServiceTestWithTestnet): 자동화 테스트 Disabled 설정 * feat: 티켓 구매시 문자 발송 (#135) * feat: 티켓 구매시 문자 발송 * refactor: smsService 메시지 부분 리팩토링 및 local 민감 정보 aws parameter store 이용 * fix: 오타 수정 및 초기화 수정 * refactor: sms 메시지 설정부 분리 * update: issue templates (#137) * refactor: 로컬 KAS 파라미터 스토어 도입 (#138) * refactor: 로컬 KAS 정보 파라미터 스토어 도입 * refactor: 콘솔 로깅 쓰레드 정보 기입 * fix: tokenId 랜덤값 증가 * test: Klaytn 관련 테스트 작성 (#141) * test: Klip 테스트 작성 * test: Kas 테스트 보충 * test(KlipServiceTest): 안 쓰는 변수 삭제 * test: 가입시 인증 로직 테스트 코드 작성 (#142) * test: artist 패키지 테스트 작성 (#143) * fix: Http Keep-Alive 설정 부여 (#140) * test: Security 패키지 테스트 작성 (#144) * refactor: 시큐리티 패키지 구조 세분화 * refactor: CustomUserDetails 추상 클래스 도입 * test: Security 테스트 작성 * test: Security 테스트 작성 * test: Global 패키지 테스트 작성 (#145) * test: Global 패키지 테스트 작성 * refactor(DateTimeUtil): 생성자 막음 * feat: Awaitility를 도입한 테스트 작성 * refactor: redisDao로 네이밍 변경 * OrderConteollerTest 추가 (#147) Co-authored-by: jungpil98 * test: EventService 테스트 (#146) * test: EventService 테스트 * test: 종속된 티켓이 없는 경우 이벤트 상세조회 케이스 추가 * fix: 티켓없는 이벤트 상세 조회 가능하도록 쿼리 수정 * test: KasService Mocking 후 티켓 테스트 케이스 추가 * chore: ToDo 제거 Co-authored-by: jungpil98 * refactor: AdminService 테스트 및 구조 개선 (#148) * refactor: AdminOrderService 도입 * test: 도메인 픽스쳐 도입 * test: EventIssueRequest 제약 조건 테스트 작성 * test(AdminServiceTest): 이벤트 발행 테스트 작성 * feat: WaitUtil 도입 * feat: 어드민 서비스 테스트 작성 * fix: valid 어노테이션 추가 * refactor: common/util -> util 패키지 네임 변경 * refactor: restTemplate 공통 구현 * fix: SecureRandom 도입 * build: jacoco 도입 (#149) * test: user controller 테스트 (#151) * test: user-controller test 코드 추가 * refactor: 미사용 엔드포인트 제거(service, repository 이력관리를 위해 남겨둠) * test: Bad Request test 추가 및 ticket 도메인 테스트 Co-authored-by: jungpil98 * test: 어드민 인수테스트 작성 (#152) * docs(Readme): 문서 초안 작성 (#153) * docs(Readme): 문서 초안 작성 * fix(Readme): 아키텍처 부분 삭제 및 API 문서화 추가 * docs(Readme): requirements 삭제 * docs(Readme): IDE에서 발췌한 ERD로 변경 Co-authored-by: jungpil98 * fix: Request key 이슈로 에러나는 현상 수정 (#155) Co-authored-by: jungpil98 * feat: TimeCheck AOP 도입 (#158) * feat: artist페이지 방명록 기능 (#157) * feat(Comment): 도메인 추가 * feat(Comment): DDL 스크립트 작성 * feat(Artist): 서비스 레이어 작성 * feat(Comment): 기능 추가 * refactor: DB 스크립트 커맨드는 대문자로 수정 * test: ArtistService 테스트 코드 추가 * test: ArtistController 테스트 작성 * refactor: 권한 분리 및 테스트 코드 수정 Co-authored-by: jungpil98 * refactor: DirtiesContext제거 하여 Truncate로 변경 (#159) Co-authored-by: jungpil98 * feat: 프로메테우스 actuator 도입 (#160) * build: actuator 의존성 추가 및 springfox 충돌 해결 * build: 프로메테우스 actuator 도입 * refactor: code smell 제네릭 적용 * docs: tech spec 업데이트 (#161) * docs: tech spec 업데이트 * fix: 오타 수정 * feat: 아티스트 별 이벤트 조회 (#162) * refactor: artistId 반환하도록 dto 수정 * feat: 아티스트 별 이벤트 조회 API 구현 * test: 컨트롤러 테스트 요청/응답 보충 * feat: 오늘 구매 가능한 이벤트 조회 API (#163) * feat: 오늘 구매 가능한 이벤트 조회 기능 구현 * refactor: EventRepositoryTest 픽스쳐 도입 * test(EventRepositoryTest): 현재 구매 가능 티켓 레포지토리 테스트 * test(EventServiceTest): 현재 구매 가능 티켓 서비스 테스트 * test(EventControllerTest): 현재 구매 가능 티켓 컨트롤러 테스트 * feat: 댓글 삭제 기능 추가 (#164) * feat: 댓글 삭제 기능 추가 * fix: PUT -> DELETE 메소드로 변경 * fix(ArtistService, Comment): 명세에 따른 로직 변경 Co-authored-by: jungpil98 * feat: 아티스트 NFT 홀더 확인 (#165) * feat: NFT 홀더 체크 기능 구현 * test: NFT 홀더 검증 테스트 작성 * refactor: kas 로직 내부로 변경 * refactor(ArtistService): 메서드 체이닝으로 수정 * test: Artist 인수테스트 작성 * fix: Security 예외처리 추가 * refactor(KasServiceMockSetup): mockserver 셋업 static으로 변경 * feat: 아티스트 상세 조회시 공지사항 및 추가정보 필드 추가 (#166) * feat: 아티스트 상세 조회시 공지사항 및 추가정보 필드 추가 * fix: DDL 잘못된 필드명 수정 * fix: 생성자 내 일부 필드 삭제 * refactor(NoticeResponse): DTO 내 Entity 사용부 DTO로 변경 * fix: query질의 alias 삭제 * fix(noticeStatus): Enumerated 어노테이션 누락 * refactor: enum 을 status 필드로 하여 ui>dto 필드에서 도메인 노출하지 않게함 Co-authored-by: jungpil98 * fix(ArtistDetailResponse): 머지 과정에서 삭제된 부분 회생 (#167) Co-authored-by: jungpil98 * fix: writtenAt 타임 스탬프로 변경 (#168) Co-authored-by: jungpil98 * fix: 트랜잭셔널 어노테이션 추가 및 아티스트 상세 조회시 id누락부 (#169) Co-authored-by: jungpil98 * fix: 아티스트 별 이벤트 가져오는 쿼리 수정 (#170) * refactor: EventDetail에 artistId 정보 추가 (#172) * feat: WebClient timeout 설정 (#171) * refactor: WebClient timeout 추가 * refactor: 예상치 못한 에러에 대한 예외 로깅 전략 수정 * refactor(KasException): 에러 메시지 내용 수정 * feat: 아티스트 댓글 조회 시 티켓 정보 반환 (#173) * feat: 각 klaytnAddress 마다 토큰 홀더인지 검증하는 기능 추가 * feat: 댓글별 아티스트 홀더인지 검사 * test: 테스트 보강 * refactor: 아티스트 댓글 TimeCheck 도입 * refactor: findMetadataByTokenIdAndTokenUri Optional 도입 * fix: Thread Interrupt 알림 추가 * docs: ERD 업데이트 (#174) * refactor: 일괄 구매 수량 옵션 추가 (#176) Co-authored-by: mrlee7 Co-authored-by: jungpil98 --- .../artist/service/ArtistService.java | 9 +- .../repository/TicketRepositoryCustom.java | 5 + .../repository/TicketRepositoryImpl.java | 40 +++- .../connectable/exception/ErrorType.java | 1 + .../order/service/OrderService.java | 57 +++-- .../order/ui/dto/OrderRequest.java | 8 + .../order/service/OrderServiceTest.java | 226 +++++++----------- 7 files changed, 169 insertions(+), 177 deletions(-) diff --git a/src/main/java/com/backend/connectable/artist/service/ArtistService.java b/src/main/java/com/backend/connectable/artist/service/ArtistService.java index c9fa232..b7bc3e8 100644 --- a/src/main/java/com/backend/connectable/artist/service/ArtistService.java +++ b/src/main/java/com/backend/connectable/artist/service/ArtistService.java @@ -139,8 +139,13 @@ private List parseHolderComments( private TicketMetadata findTicketMetadata(TokenIdentifier tokenIdentifier) { int tokenId = tokenIdentifier.getTokenId(); String tokenUri = tokenIdentifier.getTokenUri(); - return ticketRepository.findMetadataByTokenIdAndTokenUri(tokenId, tokenUri) - .orElseThrow(() -> new ConnectableException(HttpStatus.INTERNAL_SERVER_ERROR, ErrorType.TICKET_METADATA_NOT_FOUND)); + return ticketRepository + .findMetadataByTokenIdAndTokenUri(tokenId, tokenUri) + .orElseThrow( + () -> + new ConnectableException( + HttpStatus.INTERNAL_SERVER_ERROR, + ErrorType.TICKET_METADATA_NOT_FOUND)); } @Transactional diff --git a/src/main/java/com/backend/connectable/event/domain/repository/TicketRepositoryCustom.java b/src/main/java/com/backend/connectable/event/domain/repository/TicketRepositoryCustom.java index 3761ed6..2d72448 100644 --- a/src/main/java/com/backend/connectable/event/domain/repository/TicketRepositoryCustom.java +++ b/src/main/java/com/backend/connectable/event/domain/repository/TicketRepositoryCustom.java @@ -2,6 +2,7 @@ import com.backend.connectable.event.domain.Ticket; import com.backend.connectable.event.domain.TicketMetadata; +import java.util.List; import java.util.Optional; @@ -11,5 +12,9 @@ public interface TicketRepositoryCustom { Ticket findOneOnSaleOfEvent(Long eventId); + List findTicketsOnSaleOfEvent(Long eventId, Long requestedCount); + + Long countTicketsOnSaleOfEvent(Long eventId); + Optional findMetadataByTokenIdAndTokenUri(int tokenId, String tokenUri); } diff --git a/src/main/java/com/backend/connectable/event/domain/repository/TicketRepositoryImpl.java b/src/main/java/com/backend/connectable/event/domain/repository/TicketRepositoryImpl.java index 7df7206..4f422c8 100644 --- a/src/main/java/com/backend/connectable/event/domain/repository/TicketRepositoryImpl.java +++ b/src/main/java/com/backend/connectable/event/domain/repository/TicketRepositoryImpl.java @@ -9,6 +9,7 @@ import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; import javax.persistence.EntityManager; import org.springframework.stereotype.Repository; @@ -49,11 +50,40 @@ public Ticket findOneOnSaleOfEvent(Long eventId) { } @Override - public Optional findMetadataByTokenIdAndTokenUri(int tokenId, String tokenUri) { - return Optional.ofNullable(queryFactory - .select(ticket.ticketMetadata) + public List findTicketsOnSaleOfEvent(Long eventId, Long requestedCount) { + return queryFactory + .select(ticket) .from(ticket) - .where(ticket.tokenId.eq(tokenId).and(ticket.tokenUri.eq(tokenUri))) - .fetchOne()); + .where( + ticket.event + .id + .eq(eventId) + .and(ticket.ticketSalesStatus.eq(TicketSalesStatus.ON_SALE))) + .limit(requestedCount) + .fetch(); + } + + @Override + public Long countTicketsOnSaleOfEvent(Long eventId) { + return queryFactory + .select(ticket) + .from(ticket) + .where( + ticket.event + .id + .eq(eventId) + .and(ticket.ticketSalesStatus.eq(TicketSalesStatus.ON_SALE))) + .stream() + .count(); + } + + @Override + public Optional findMetadataByTokenIdAndTokenUri(int tokenId, String tokenUri) { + return Optional.ofNullable( + queryFactory + .select(ticket.ticketMetadata) + .from(ticket) + .where(ticket.tokenId.eq(tokenId).and(ticket.tokenUri.eq(tokenUri))) + .fetchOne()); } } diff --git a/src/main/java/com/backend/connectable/exception/ErrorType.java b/src/main/java/com/backend/connectable/exception/ErrorType.java index 553ab01..8e83a4b 100644 --- a/src/main/java/com/backend/connectable/exception/ErrorType.java +++ b/src/main/java/com/backend/connectable/exception/ErrorType.java @@ -44,6 +44,7 @@ public enum ErrorType { ORDER_TO_REFUND_UNAVAILABLE("ORDER-003", "REFUND 상태로 변경이 불가합니다."), ORDER_TO_TRANSFER_SUCCESS_UNAVAILABLE("ORDER-004", "TRANSFER SUCCESS 상태로 변경이 불가합니다."), ORDER_TO_TRANSFER_FAIL_UNAVAILABLE("ORDER-005", "TRANSFER FAIL 상태로 변경이 불가합니다."), + LESS_NUMBER_OF_ORDER_REQUIRED("ORDER-006", "판매 가능한 티켓 수보다 많이 구매 요청을 하였습니다."), ENTRANCE_ALREADY_DONE("ENTRANCE-001", "입장 처리가 완료된 티켓입니다."), ENTRANCE_AUTHORIZATION_NEEDED("ENTRANCE-002", "입장 처리에 대한 권한이 없습니다"), diff --git a/src/main/java/com/backend/connectable/order/service/OrderService.java b/src/main/java/com/backend/connectable/order/service/OrderService.java index 69aa218..a47231a 100644 --- a/src/main/java/com/backend/connectable/order/service/OrderService.java +++ b/src/main/java/com/backend/connectable/order/service/OrderService.java @@ -41,16 +41,6 @@ public OrderResponse createOrder( return OrderResponse.from("success"); } - public List getOrderDetailList(ConnectableUserDetails userDetails) { - String klaytnAddress = userDetails.getKlaytnAddress(); - List orderDetailResponses = - orderRepository.getOrderDetailList(klaytnAddress); - - return orderDetailResponses.stream() - .map(OrderMapper.INSTANCE::ticketOrderDetailToResponse) - .collect(Collectors.toList()); - } - private User findUser(String klaytnAddress) { return userRepository .findByKlaytnAddress(klaytnAddress) @@ -61,8 +51,7 @@ private User findUser(String klaytnAddress) { } private Order generateOrder(User user, OrderRequest orderRequest) { - List orderDetails = - generateOrderDetails(orderRequest.getTicketIds(), orderRequest.getEventId()); + List orderDetails = generateOrderDetails(orderRequest); Order order = Order.builder() .user(user) @@ -73,28 +62,48 @@ private Order generateOrder(User user, OrderRequest orderRequest) { return order; } - private List generateOrderDetails(List ticketIds, Long eventId) { - if (checkRandomTicketSelection(ticketIds)) { - return generateRandomTicketOrderDetail(eventId); + private List generateOrderDetails(OrderRequest orderRequest) { + if (orderRequest.isRandomTicketSelection()) { + return generateRandomTicketsOrderDetail(orderRequest); } + return generateRequestedTicketsOrderDetail(orderRequest); + } - List tickets = ticketRepository.findAllById(ticketIds); + private List generateRandomTicketsOrderDetail(OrderRequest orderRequest) { + Long eventId = orderRequest.getEventId(); + Long requestedCount = orderRequest.getRequestedTicketCount(); + validateRequestedCount(eventId, requestedCount); + List tickets = ticketRepository.findTicketsOnSaleOfEvent(eventId, requestedCount); tickets.forEach(Ticket::toPending); return tickets.stream().map(this::toOrderDetail).collect(Collectors.toList()); } - private boolean checkRandomTicketSelection(List ticketIds) { - return ticketIds.contains(0L) && ticketIds.size() == 1; + private void validateRequestedCount(Long eventId, Long requestedCount) { + Long onSaleTicketCount = ticketRepository.countTicketsOnSaleOfEvent(eventId); + if (requestedCount > onSaleTicketCount) { + throw new ConnectableException( + HttpStatus.BAD_REQUEST, ErrorType.LESS_NUMBER_OF_ORDER_REQUIRED); + } } - private List generateRandomTicketOrderDetail(Long eventId) { - Ticket ticket = ticketRepository.findOneOnSaleOfEvent(eventId); - ticket.toPending(); - OrderDetail orderDetail = toOrderDetail(ticket); - return List.of(orderDetail); + private List generateRequestedTicketsOrderDetail(OrderRequest orderRequest) { + List ticketIds = orderRequest.getTicketIds(); + List tickets = ticketRepository.findAllById(ticketIds); + tickets.forEach(Ticket::toPending); + return tickets.stream().map(this::toOrderDetail).collect(Collectors.toList()); } private OrderDetail toOrderDetail(Ticket ticket) { - return new OrderDetail(OrderStatus.REQUESTED, null, ticket); + return OrderDetail.builder().orderStatus(OrderStatus.REQUESTED).ticket(ticket).build(); + } + + public List getOrderDetailList(ConnectableUserDetails userDetails) { + String klaytnAddress = userDetails.getKlaytnAddress(); + List orderDetailResponses = + orderRepository.getOrderDetailList(klaytnAddress); + + return orderDetailResponses.stream() + .map(OrderMapper.INSTANCE::ticketOrderDetailToResponse) + .collect(Collectors.toList()); } } diff --git a/src/main/java/com/backend/connectable/order/ui/dto/OrderRequest.java b/src/main/java/com/backend/connectable/order/ui/dto/OrderRequest.java index 35ff40b..fa6d96a 100644 --- a/src/main/java/com/backend/connectable/order/ui/dto/OrderRequest.java +++ b/src/main/java/com/backend/connectable/order/ui/dto/OrderRequest.java @@ -30,4 +30,12 @@ public class OrderRequest { private Long eventId; private List ticketIds; + + public boolean isRandomTicketSelection() { + return ticketIds.contains(0L); + } + + public Long getRequestedTicketCount() { + return (long) ticketIds.size(); + } } diff --git a/src/test/java/com/backend/connectable/order/service/OrderServiceTest.java b/src/test/java/com/backend/connectable/order/service/OrderServiceTest.java index 2f799c7..bc0aedc 100644 --- a/src/test/java/com/backend/connectable/order/service/OrderServiceTest.java +++ b/src/test/java/com/backend/connectable/order/service/OrderServiceTest.java @@ -1,6 +1,11 @@ package com.backend.connectable.order.service; +import static com.backend.connectable.fixture.ArtistFixture.createArtistBigNaughty; +import static com.backend.connectable.fixture.EventFixture.createEvent; +import static com.backend.connectable.fixture.TicketFixture.createTicketWithSalesStatus; +import static com.backend.connectable.fixture.UserFixture.createUserMrLee; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import com.backend.connectable.artist.domain.Artist; @@ -8,6 +13,7 @@ import com.backend.connectable.event.domain.*; import com.backend.connectable.event.domain.repository.EventRepository; import com.backend.connectable.event.domain.repository.TicketRepository; +import com.backend.connectable.exception.ConnectableException; import com.backend.connectable.order.domain.OrderStatus; import com.backend.connectable.order.domain.repository.OrderDetailRepository; import com.backend.connectable.order.domain.repository.OrderRepository; @@ -17,9 +23,7 @@ import com.backend.connectable.security.custom.ConnectableUserDetails; import com.backend.connectable.user.domain.User; import com.backend.connectable.user.domain.repository.UserRepository; -import java.time.LocalDateTime; import java.util.Arrays; -import java.util.HashMap; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -47,21 +51,12 @@ class OrderServiceTest { @Autowired OrderService orderService; - private User user; - private Artist artist; - private Event event; - private TicketMetadata ticket1Metadata; - private Ticket ticket1; - private TicketMetadata ticket2Metadata; - private Ticket ticket2; - private TicketMetadata ticket3Metadata; - private Ticket ticket3; - - private final String userKlaytnAddress = "0x12345678"; - private final String userNickname = "leejp"; - private final String userPhoneNumber = "010-3333-7777"; - private final boolean userPrivacyAgreement = true; - private final boolean userIsActive = true; + private final User user = createUserMrLee(); + private final Artist artist = createArtistBigNaughty(); + private final Event event = createEvent(artist); + private final Ticket ticket1 = createTicketWithSalesStatus(event, 1, TicketSalesStatus.ON_SALE); + private final Ticket ticket2 = createTicketWithSalesStatus(event, 2, TicketSalesStatus.ON_SALE); + private final Ticket ticket3 = createTicketWithSalesStatus(event, 3, TicketSalesStatus.ON_SALE); @BeforeEach void setUp() { @@ -72,125 +67,10 @@ void setUp() { artistRepository.deleteAll(); userRepository.deleteAll(); - user = - User.builder() - .klaytnAddress(userKlaytnAddress) - .nickname(userNickname) - .phoneNumber(userPhoneNumber) - .privacyAgreement(userPrivacyAgreement) - .isActive(userIsActive) - .build(); - - artist = - Artist.builder() - .bankCompany("NH") - .bankAccount("9000000000099") - .artistName("빅나티") - .email("bignaughty@gmail.com") - .password("temptemp1234") - .phoneNumber("01012345678") - .artistImage("https://image.url") - .build(); - - event = - Event.builder() - .description("조엘의 콘서트 at Connectable") - .salesFrom(LocalDateTime.of(2022, 7, 12, 0, 0)) - .salesTo(LocalDateTime.of(2022, 7, 30, 0, 0)) - .contractAddress("0x123456") - .eventName("조엘의 콘서트") - .eventImage("https://image.url") - .twitterUrl("https://github.com/joelonsw") - .instagramUrl("https://www.instagram.com/jyoung_with/") - .webpageUrl("https://papimon.tistory.com/") - .startTime(LocalDateTime.of(2022, 8, 1, 18, 0)) - .endTime(LocalDateTime.of(2022, 8, 1, 19, 0)) - .salesOption(SalesOption.FLAT_PRICE) - .artist(artist) - .build(); - - ticket1Metadata = - TicketMetadata.builder() - .name("조엘 콘서트 #1") - .description("조엘의 콘서트 at Connectable") - .image( - "https://connectable-events.s3.ap-northeast-2.amazonaws.com/ticket_test1.png") - .attributes( - new HashMap<>() { - { - put("Background", "Yellow"); - put("Artist", "Joel"); - put("Seat", "A6"); - } - }) - .build(); - - ticket1 = - Ticket.builder() - .event(event) - .tokenUri( - "https://connectable-events.s3.ap-northeast-2.amazonaws.com/json/1.json") - .price(100000) - .ticketSalesStatus(TicketSalesStatus.ON_SALE) - .ticketMetadata(ticket1Metadata) - .build(); - - ticket2Metadata = - TicketMetadata.builder() - .name("조엘 콘서트 #2") - .description("조엘의 콘서트 at Connectable") - .image( - "https://connectable-events.s3.ap-northeast-2.amazonaws.com/ticket_test2.png") - .attributes( - new HashMap<>() { - { - put("Background", "Yellow"); - put("Artist", "Joel"); - put("Seat", "A5"); - } - }) - .build(); - - ticket2 = - Ticket.builder() - .event(event) - .tokenUri( - "https://connectable-events.s3.ap-northeast-2.amazonaws.com/json/2.json") - .price(100000) - .ticketSalesStatus(TicketSalesStatus.ON_SALE) - .ticketMetadata(ticket2Metadata) - .build(); - - ticket3Metadata = - TicketMetadata.builder() - .name("조엘 콘서트 #3") - .description("조엘의 콘서트 at Connectable") - .image( - "https://connectable-events.s3.ap-northeast-2.amazonaws.com/ticket_test3.png") - .attributes( - new HashMap<>() { - { - put("Background", "Yellow"); - put("Artist", "Joel"); - put("Seat", "A5"); - } - }) - .build(); - - ticket3 = - Ticket.builder() - .event(event) - .tokenUri( - "https://connectable-events.s3.ap-northeast-2.amazonaws.com/json/3.json") - .price(100000) - .ticketSalesStatus(TicketSalesStatus.ON_SALE) - .ticketMetadata(ticket3Metadata) - .build(); - userRepository.save(user); artistRepository.save(artist); eventRepository.save(event); - ticketRepository.saveAll(Arrays.asList(ticket1, ticket2, ticket3)); + ticketRepository.saveAll(List.of(ticket1, ticket2, ticket3)); } @DisplayName("티켓을 구매하였을때, 주문정보 및 주문상세정보가 등록된다.") @@ -201,8 +81,8 @@ void createOrder() { new ConnectableUserDetails(user.getKlaytnAddress()); OrderRequest orderRequest = new OrderRequest( - "이정필", - "010-3333-7777", + user.getNickname(), + user.getPhoneNumber(), event.getId(), Arrays.asList(ticket1.getId(), ticket2.getId())); @@ -226,35 +106,44 @@ void getOrderDetailList() { new ConnectableUserDetails(user.getKlaytnAddress()); OrderRequest orderRequest1 = new OrderRequest( - "이정필", - "010-3333-7777", + user.getNickname(), + user.getPhoneNumber(), event.getId(), - Arrays.asList(ticket1.getId(), ticket2.getId())); + List.of(ticket1.getId(), ticket2.getId())); OrderRequest orderRequest2 = new OrderRequest( - "이정필", "010-3333-7777", event.getId(), Arrays.asList(ticket3.getId())); - - // when + user.getNickname(), + user.getPhoneNumber(), + event.getId(), + List.of(ticket3.getId())); orderService.createOrder(connectableUserDetails, orderRequest1); orderService.createOrder(connectableUserDetails, orderRequest2); + + // when List orderDetailResponses = orderService.getOrderDetailList(connectableUserDetails); // then assertEquals(3L, orderDetailResponses.size()); + assertThat(orderDetailResponses.get(0).getTicketSalesStatus()) .isEqualTo(TicketSalesStatus.PENDING); assertThat(orderDetailResponses.get(0).getOrderStatus()).isEqualTo(OrderStatus.REQUESTED); assertThat(orderDetailResponses.get(0).getModifiedDate()).isNotNull(); - assertThat(orderDetailResponses.get(0).getTicketMetadata().getName()).contains("조엘"); - assertThat(orderDetailResponses.get(0).getEventId()).isNotNull(); - assertThat(orderDetailResponses.get(0).getPrice()).isEqualTo(100000); + assertThat(orderDetailResponses.get(0).getTicketMetadata().getName()) + .isEqualTo(ticket1.getTicketMetadata().getName()); + assertThat(orderDetailResponses.get(0).getEventId()).isEqualTo(event.getId()); + assertThat(orderDetailResponses.get(0).getPrice()).isEqualTo(ticket1.getPrice()); + assertThat(orderDetailResponses.get(1).getTicketSalesStatus()) .isEqualTo(TicketSalesStatus.PENDING); assertThat(orderDetailResponses.get(1).getOrderStatus()).isEqualTo(OrderStatus.REQUESTED); assertThat(orderDetailResponses.get(1).getModifiedDate()).isNotNull(); - assertThat(orderDetailResponses.get(1).getEventId()).isNotNull(); - assertThat(orderDetailResponses.get(1).getPrice()).isEqualTo(100000); + assertThat(orderDetailResponses.get(1).getTicketMetadata().getName()) + .isEqualTo(ticket2.getTicketMetadata().getName()); + assertThat(orderDetailResponses.get(1).getEventId()).isEqualTo(event.getId()); + assertThat(orderDetailResponses.get(1).getPrice()).isEqualTo(ticket2.getPrice()); + assertThat(orderDetailResponses.get(0).getModifiedDate()) .isGreaterThanOrEqualTo(orderDetailResponses.get(1).getModifiedDate()); } @@ -277,4 +166,49 @@ void ticketIdZero() { Ticket updatedTicket1 = ticketRepository.findById(ticket1.getId()).get(); assertThat(updatedTicket1.getTicketSalesStatus()).isEqualTo(TicketSalesStatus.PENDING); } + + @DisplayName("List에 담긴 0의 갯수만큼 event에 대응되는 티켓중 판매가능한 티켓을 orderDetail로 주문한다.") + @Test + void ticketIdMultipleZero() { + // given + ConnectableUserDetails connectableUserDetails = + new ConnectableUserDetails(user.getKlaytnAddress()); + OrderRequest orderRequest = + new OrderRequest( + user.getNickname(), + user.getPhoneNumber(), + event.getId(), + List.of(0L, 0L, 0L)); + + // when + OrderResponse orderResponse = + orderService.createOrder(connectableUserDetails, orderRequest); + + // then + assertThat(orderResponse.getStatus()).isEqualTo("success"); + Ticket updatedTicket1 = ticketRepository.findById(ticket1.getId()).get(); + Ticket updatedTicket2 = ticketRepository.findById(ticket2.getId()).get(); + Ticket updatedTicket3 = ticketRepository.findById(ticket3.getId()).get(); + assertThat(updatedTicket1.getTicketSalesStatus()).isEqualTo(TicketSalesStatus.PENDING); + assertThat(updatedTicket2.getTicketSalesStatus()).isEqualTo(TicketSalesStatus.PENDING); + assertThat(updatedTicket3.getTicketSalesStatus()).isEqualTo(TicketSalesStatus.PENDING); + } + + @DisplayName("List에 담긴 0의 갯수가 event에 대응되는 티켓중 판매가능한 티켓의 수보다 크다면 예외가 발생한다.") + @Test + void ticketIdMultipleZeroOverLimit() { + // given + ConnectableUserDetails connectableUserDetails = + new ConnectableUserDetails(user.getKlaytnAddress()); + OrderRequest orderRequest = + new OrderRequest( + user.getNickname(), + user.getPhoneNumber(), + event.getId(), + List.of(0L, 0L, 0L, 0L, 0L, 0L)); + + // when & then + assertThatThrownBy(() -> orderService.createOrder(connectableUserDetails, orderRequest)) + .isInstanceOf(ConnectableException.class); + } }