Skip to content

Commit

Permalink
feat: 새로운 Ticket 도메인 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
seokjin8678 committed Jun 11, 2024
1 parent 6c29b0b commit 9503869
Show file tree
Hide file tree
Showing 18 changed files with 1,361 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ public enum ErrorCode {
TICKET_CANNOT_RESERVE_STAGE_START("공연의 시작 시간 이후로 예매할 수 없습니다."),
INVALID_STUDENT_VERIFICATION_CODE("올바르지 않은 학생 인증 코드입니다."),
DELETE_CONSTRAINT_FESTIVAL("공연이 등록된 축제는 삭제할 수 없습니다."),
DELETE_CONSTRAINT_STAGE("티켓이 등록된 공연은 삭제할 수 없습니다."),
DELETE_CONSTRAINT_SCHOOL("학생 또는 축제에 등록된 학교는 삭제할 수 없습니다."), // @deprecate
DUPLICATE_SCHOOL("이미 존재하는 학교 정보입니다."), // @deprecate
VALIDATION_FAIL("검증이 실패하였습니다."),
Expand All @@ -47,6 +46,12 @@ public enum ErrorCode {
OPEN_ID_INVALID_TOKEN("잘못된 OpenID 토큰입니다."),
NOT_SUPPORT_FILE_EXTENSION("해당 파일의 확장자는 허용되지 않습니다."),
DUPLICATE_ARTIST_NAME("이미 존재하는 아티스트의 이름입니다."),
RESERVE_TICKET_BEFORE_TICKET_OPEN_TIME("티켓 예매 시간 이전에는 예매 할 수 없습니다."),
RESERVE_TICKET_NOT_SCHOOL_STUDENT("해당 티켓의 예매는 소속된 학교를 다니는 재학생만 가능합니다."),
STAGE_UPDATE_CONSTRAINT_EXISTS_TICKET("티켓이 등록된 공연은 수정할 수 없습니다."),
STAGE_DELETE_CONSTRAINT_EXISTS_TICKET("티켓이 등록된 공연은 삭제할 수 없습니다."),
STAGE_TICKET_DELETE_CONSTRAINT_TICKET_OPEN_TIME("티켓 오픈 시간 이후에는 티켓을 삭제할 수 없습니다."),
ONLY_STAGE_TICKETING_SINGLE_TYPE("공연 당 하나의 유형의 티켓에 대해서만 예매가 가능합니다."),

// 401
EXPIRED_AUTH_TOKEN("만료된 로그인 토큰입니다."),
Expand Down Expand Up @@ -84,6 +89,7 @@ public enum ErrorCode {
OAUTH2_INVALID_REQUEST("알 수 없는 OAuth2 에러가 발생했습니다."),
OPEN_ID_PROVIDER_NOT_RESPONSE("OpenID 제공자 서버에 문제가 발생했습니다."),
FILE_UPLOAD_ERROR("파일 업로드 중 에러가 발생했습니다."),
REDIS_ERROR("Redis에 문제가 발생했습니다."),
;

private final String message;
Expand Down
18 changes: 10 additions & 8 deletions backend/src/main/java/com/festago/stage/domain/Stage.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import com.festago.common.exception.ErrorCode;
import com.festago.common.util.Validator;
import com.festago.festival.domain.Festival;
import com.festago.ticket.domain.Ticket;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
Expand All @@ -18,6 +17,7 @@
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.AccessLevel;
Expand All @@ -41,9 +41,6 @@ public class Stage extends BaseTimeEntity {
@ManyToOne(fetch = FetchType.LAZY)
private Festival festival;

@OneToMany(mappedBy = "stage", fetch = FetchType.LAZY)
private List<Ticket> tickets = new ArrayList<>();

@OneToMany(fetch = FetchType.LAZY, mappedBy = "stageId", orphanRemoval = true,
cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
private List<StageArtist> artists = new ArrayList<>();
Expand Down Expand Up @@ -85,6 +82,10 @@ public boolean isStart(LocalDateTime currentTime) {
return currentTime.isAfter(startTime);
}

public boolean isBeforeTicketOpenTime(LocalDateTime currentTime) {
return currentTime.isBefore(ticketOpenTime);
}

public void changeTime(LocalDateTime startTime, LocalDateTime ticketOpenTime) {
validateTime(startTime, ticketOpenTime, this.festival);
this.startTime = startTime;
Expand All @@ -110,6 +111,11 @@ public List<Long> getArtistIds() {
.toList();
}

// 디미터 법칙에 어긋나지만, n+1을 회피하고, fetch join을 생략하며 주인을 검사하기 위해 getter 체이닝 사용
public boolean isSchoolStage(Long schoolId) {
return Objects.equals(getFestival().getSchool().getId(), schoolId);
}

public Long getId() {
return id;
}
Expand All @@ -125,8 +131,4 @@ public LocalDateTime getTicketOpenTime() {
public Festival getFestival() {
return festival;
}

public List<Ticket> getTickets() {
return tickets;
}
}
106 changes: 106 additions & 0 deletions backend/src/main/java/com/festago/ticket/domain/NewTicket.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.festago.ticket.domain;

import com.festago.common.domain.BaseTimeEntity;
import com.festago.common.util.Validator;
import com.festago.ticketing.domain.Booker;
import com.festago.ticketing.domain.ReserveTicket;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;
import java.time.LocalDateTime;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

// TODO NewTicket -> Ticket 이름 변경할 것
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class NewTicket extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;

protected Long schoolId;

@Enumerated(EnumType.STRING)
@Column(columnDefinition = "varchar")
protected TicketExclusive ticketExclusive;

protected int amount = 0;

/**
* 사용자가 최대 예매할 수 있는 티켓의 개수
*/
protected int maxReserveAmount = 1;

protected NewTicket(Long id, Long schoolId, TicketExclusive ticketExclusive) {
Validator.notNull(schoolId, "schoolId");
Validator.notNull(ticketExclusive, "ticketExclusive");
this.id = id;
this.schoolId = schoolId;
this.ticketExclusive = ticketExclusive;
}

protected void changeAmount(int amount) {
Validator.notNegative(amount, "amount");
this.amount = amount;
}

public boolean isStudentOnly() {
return ticketExclusive == TicketExclusive.STUDENT;
}

public boolean isSchoolStudent(Booker booker) {
return Objects.equals(this.schoolId, booker.getSchoolId());
}

public void changeMaxReserveAmount(int maxReserveAmount) {
Validator.minValue(maxReserveAmount, 1, "maxReserveAmount");
this.maxReserveAmount = maxReserveAmount;
}

public abstract void validateReserve(Booker booker, LocalDateTime currentTime);

/**
* 티켓을 예매한다. 해당 메서드를 호출하기 전 반드시 validateReserve() 메서드를 호출해야 한다.<br/> 반환된 ReserveTicket은 영속되지 않았으므로, 반드시 영속시켜야 한다.
*
* @param booker 예매할 사용자
* @param sequence 예매할 티켓의 순번
* @return 영속되지 않은 상태의 ReserveTicket
*/
public abstract ReserveTicket reserve(Booker booker, int sequence);

public abstract LocalDateTime getTicketingEndTime();

public boolean isEmptyAmount() {
return amount <= 0;
}

public Long getId() {
return id;
}

public Long getSchoolId() {
return schoolId;
}

public TicketExclusive getTicketExclusive() {
return ticketExclusive;
}

public int getAmount() {
return amount;
}

public int getMaxReserveAmount() {
return maxReserveAmount;
}
}
11 changes: 11 additions & 0 deletions backend/src/main/java/com/festago/ticket/domain/NewTicketType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.festago.ticket.domain;

// TODO NewTicket -> Ticket 이름 변경할 것

/**
* NewTicket의 구현체의 DiscriminatorValue 어노테이션의 속성의 이름과 반드시 똑같이 할 것!
*/
public enum NewTicketType {
STAGE,
;
}
117 changes: 117 additions & 0 deletions backend/src/main/java/com/festago/ticket/domain/StageTicket.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.festago.ticket.domain;

import com.festago.common.exception.BadRequestException;
import com.festago.common.exception.ErrorCode;
import com.festago.common.exception.UnauthorizedException;
import com.festago.common.util.Validator;
import com.festago.stage.domain.Stage;
import com.festago.ticketing.domain.Booker;
import com.festago.ticketing.domain.ReserveTicket;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.ManyToOne;
import java.time.LocalDateTime;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@Entity
@DiscriminatorValue("STAGE")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class StageTicket extends NewTicket {

private static final int EARLY_ENTRY_LIMIT = 12;

@ManyToOne(fetch = FetchType.LAZY)
private Stage stage;

@Embedded
private StageTicketEntryTimes ticketEntryTimes = new StageTicketEntryTimes();

public StageTicket(Long schoolId, TicketExclusive ticketType, Stage stage) {
this(null, schoolId, ticketType, stage);
}

public StageTicket(Long id, Long schoolId, TicketExclusive ticketType, Stage stage) {
super(id, schoolId, ticketType);
validate(schoolId, stage);
this.stage = stage;
}

private void validate(Long schoolId, Stage stage) {
Validator.notNull(stage, "stage");
if (!stage.isSchoolStage(schoolId)) {
throw new UnauthorizedException(ErrorCode.NOT_ENOUGH_PERMISSION);
}
}

@Override
public LocalDateTime getTicketingEndTime() {
return stage.getStartTime();
}

@Override
public void validateReserve(Booker booker, LocalDateTime currentTime) {
if (isStudentOnly() && !isSchoolStudent(booker)) {
throw new BadRequestException(ErrorCode.RESERVE_TICKET_NOT_SCHOOL_STUDENT);
}
if (stage.isStart(currentTime)) {
throw new BadRequestException(ErrorCode.TICKET_CANNOT_RESERVE_STAGE_START);
}
if (stage.isBeforeTicketOpenTime(currentTime)) {
throw new BadRequestException(ErrorCode.RESERVE_TICKET_BEFORE_TICKET_OPEN_TIME);
}
}

@Override
public ReserveTicket reserve(Booker booker, int sequence) {
LocalDateTime entryTime = ticketEntryTimes.calculateEntryTime(sequence);
return new ReserveTicket(booker.getMemberId(), NewTicketType.STAGE, id, sequence, entryTime);
}

public void addTicketEntryTime(Long schoolId, LocalDateTime currentTime, LocalDateTime entryTime, int amount) {
validateSchoolOwner(schoolId);
validateEntryTime(currentTime, entryTime);
ticketEntryTimes.add(new StageTicketEntryTime(id, entryTime, amount));
changeAmount(ticketEntryTimes.getTotalAmount());
}

private void validateSchoolOwner(Long schoolId) {
if (!Objects.equals(this.schoolId, schoolId)) {
throw new UnauthorizedException(ErrorCode.NOT_ENOUGH_PERMISSION);
}
}

private void validateEntryTime(LocalDateTime currentTime, LocalDateTime entryTime) {
if (!stage.isBeforeTicketOpenTime(currentTime)) {
throw new BadRequestException(ErrorCode.INVALID_TICKET_CREATE_TIME);
}
if (stage.isBeforeTicketOpenTime(entryTime)) {
throw new BadRequestException(ErrorCode.EARLY_TICKET_ENTRY_THAN_OPEN);
}
if (stage.isStart(entryTime)) {
throw new BadRequestException(ErrorCode.LATE_TICKET_ENTRY_TIME);
}
if (!stage.isStart(entryTime.plusHours(EARLY_ENTRY_LIMIT))) {
throw new BadRequestException(ErrorCode.EARLY_TICKET_ENTRY_TIME);
}
}

public boolean deleteTicketEntryTime(Long schoolId, LocalDateTime currentTime, LocalDateTime entryTime) {
validateSchoolOwner(schoolId);
if (!stage.isBeforeTicketOpenTime(currentTime)) {
throw new BadRequestException(ErrorCode.STAGE_TICKET_DELETE_CONSTRAINT_TICKET_OPEN_TIME);
}
boolean isDeleted = ticketEntryTimes.remove(entryTime);
if (isDeleted) {
changeAmount(ticketEntryTimes.getTotalAmount());
}
return isDeleted;
}

public Stage getStage() {
return stage;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.festago.ticket.domain;

import com.festago.common.domain.BaseTimeEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class StageTicketEntryTime extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private Long stageTicketId;

private LocalDateTime entryTime;

private int amount;

public StageTicketEntryTime(Long stageTicketId, LocalDateTime entryTime, int amount) {
this(null, stageTicketId, entryTime, amount);
}

public StageTicketEntryTime(Long id, Long stageTicketId, LocalDateTime entryTime, int amount) {
this.id = id;
this.stageTicketId = stageTicketId;
this.entryTime = entryTime;
this.amount = amount;
}

public Long getId() {
return id;
}

public Long getStageTicketId() {
return stageTicketId;
}

public LocalDateTime getEntryTime() {
return entryTime;
}

public int getAmount() {
return amount;
}
}
Loading

0 comments on commit 9503869

Please sign in to comment.