Skip to content

Commit a9db902

Browse files
authored
merge: 도어용 유저 티켓 검증 API 구현 - #135
feat: 도어용 유저 티켓 검증 API 구현 - #135
2 parents c5fdbb5 + f311acb commit a9db902

File tree

13 files changed

+148
-28
lines changed

13 files changed

+148
-28
lines changed

src/main/java/com/permitseoul/permitserver/domain/ticket/api/controller/TicketController.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.permitseoul.permitserver.domain.ticket.api.controller;
22

3+
import com.permitseoul.permitserver.domain.ticket.api.dto.req.TicketConfirmRequest;
34
import com.permitseoul.permitserver.domain.ticket.api.service.TicketService;
45
import com.permitseoul.permitserver.domain.ticket.core.domain.TicketStatus;
56
import com.permitseoul.permitserver.domain.ticket.core.domain.TicketUsability;
@@ -8,12 +9,11 @@
89
import com.permitseoul.permitserver.global.response.ApiResponseUtil;
910
import com.permitseoul.permitserver.global.response.BaseResponse;
1011
import com.permitseoul.permitserver.global.response.code.SuccessCode;
12+
import jakarta.validation.Valid;
1113
import lombok.RequiredArgsConstructor;
1214
import org.springframework.data.repository.query.Param;
1315
import org.springframework.http.ResponseEntity;
14-
import org.springframework.web.bind.annotation.GetMapping;
15-
import org.springframework.web.bind.annotation.RequestMapping;
16-
import org.springframework.web.bind.annotation.RestController;
16+
import org.springframework.web.bind.annotation.*;
1717

1818
import java.time.LocalDateTime;
1919

@@ -31,11 +31,20 @@ public ResponseEntity<BaseResponse<?>> getEventTicketInfo(
3131
return ApiResponseUtil.success(SuccessCode.OK, ticketService.getEventTicketInfo(eventId, LocalDateTime.now()));
3232
}
3333

34-
//구매한 티켓 정보 조회
34+
//구매한 티켓 정보 조회 api
3535
@GetMapping("/user")
3636
public ResponseEntity<BaseResponse<?>> getUserBuyTicketInfo(
3737
@UserIdHeader final Long userId
3838
) {
3939
return ApiResponseUtil.success(SuccessCode.OK, ticketService.getUserBuyTicketInfo(userId));
4040
}
41+
42+
//도어용 티켓 검증 api
43+
@PostMapping("/confirm")
44+
public ResponseEntity<BaseResponse<?>> getUserBuyTicketInfo(
45+
@RequestBody @Valid TicketConfirmRequest ticketConfirmRequest
46+
) {
47+
ticketService.confirmTicket(ticketConfirmRequest.ticketCode(), ticketConfirmRequest.checkCode());
48+
return ApiResponseUtil.success(SuccessCode.OK);
49+
}
4150
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.permitseoul.permitserver.domain.ticket.api.dto.req;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
5+
public record TicketConfirmRequest(
6+
@NotBlank(message = "ticketCode가 비어있습니다.")
7+
String ticketCode,
8+
9+
@NotBlank(message = "checkCode가 비어있습니다.")
10+
String checkCode
11+
) {
12+
}

src/main/java/com/permitseoul/permitserver/domain/ticket/api/dto/EventTicketInfoResponse.java renamed to src/main/java/com/permitseoul/permitserver/domain/ticket/api/dto/res/EventTicketInfoResponse.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.permitseoul.permitserver.domain.ticket.api.dto;
1+
package com.permitseoul.permitserver.domain.ticket.api.dto.res;
22

33
import java.util.List;
44

src/main/java/com/permitseoul/permitserver/domain/ticket/api/dto/UserBuyTicketInfo.java renamed to src/main/java/com/permitseoul/permitserver/domain/ticket/api/dto/res/UserBuyTicketInfoResponse.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
package com.permitseoul.permitserver.domain.ticket.api.dto;
1+
package com.permitseoul.permitserver.domain.ticket.api.dto.res;
22

33

44
import java.util.List;
55

6-
public record UserBuyTicketInfo(
6+
public record UserBuyTicketInfoResponse(
77
List<Order> orders
88
) {
99
public record Order(
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.permitseoul.permitserver.domain.ticket.api.exception;
2+
3+
import com.permitseoul.permitserver.global.response.code.ErrorCode;
4+
5+
public class ConflictTicketException extends TicketApiException {
6+
public ConflictTicketException(ErrorCode errorCode) {
7+
super(errorCode);
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.permitseoul.permitserver.domain.ticket.api.exception;
2+
3+
import com.permitseoul.permitserver.global.response.code.ErrorCode;
4+
5+
public class DateTicketException extends TicketApiException {
6+
public DateTicketException(ErrorCode errorCode) {
7+
super(errorCode);
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.permitseoul.permitserver.domain.ticket.api.exception;
2+
3+
import com.permitseoul.permitserver.global.response.code.ErrorCode;
4+
5+
public class IllegalTicketException extends TicketApiException {
6+
public IllegalTicketException(ErrorCode errorCode) {
7+
super(errorCode);
8+
}
9+
}

src/main/java/com/permitseoul/permitserver/domain/ticket/api/service/TicketService.java

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,21 @@
22

33
import com.permitseoul.permitserver.domain.event.core.component.EventRetriever;
44
import com.permitseoul.permitserver.domain.event.core.domain.Event;
5+
import com.permitseoul.permitserver.domain.event.core.exception.EventNotfoundException;
56
import com.permitseoul.permitserver.domain.payment.core.component.PaymentRetriever;
67
import com.permitseoul.permitserver.domain.payment.core.domain.Payment;
7-
import com.permitseoul.permitserver.domain.ticket.api.dto.EventTicketInfoResponse;
8-
import com.permitseoul.permitserver.domain.ticket.api.dto.UserBuyTicketInfo;
8+
import com.permitseoul.permitserver.domain.ticket.api.dto.res.EventTicketInfoResponse;
9+
import com.permitseoul.permitserver.domain.ticket.api.dto.res.UserBuyTicketInfoResponse;
10+
import com.permitseoul.permitserver.domain.ticket.api.exception.ConflictTicketException;
11+
import com.permitseoul.permitserver.domain.ticket.api.exception.DateTicketException;
12+
import com.permitseoul.permitserver.domain.ticket.api.exception.IllegalTicketException;
913
import com.permitseoul.permitserver.domain.ticket.api.exception.NotFoundTicketException;
1014
import com.permitseoul.permitserver.domain.ticket.core.component.TicketRetriever;
15+
import com.permitseoul.permitserver.domain.ticket.core.component.TicketUpdater;
1116
import com.permitseoul.permitserver.domain.ticket.core.domain.Ticket;
1217
import com.permitseoul.permitserver.domain.ticket.core.domain.TicketStatus;
18+
import com.permitseoul.permitserver.domain.ticket.core.domain.entity.TicketEntity;
19+
import com.permitseoul.permitserver.domain.ticket.core.exception.TicketNotFoundException;
1320
import com.permitseoul.permitserver.domain.ticketround.core.component.TicketRoundRetriever;
1421
import com.permitseoul.permitserver.domain.ticketround.core.domain.TicketRound;
1522
import com.permitseoul.permitserver.domain.tickettype.core.component.TicketTypeRetriever;
@@ -41,6 +48,7 @@ public class TicketService {
4148
private final PaymentRetriever paymentRetriever;
4249

4350
private static final String ORDER_DATE_FORMAT = "yyyy-MM-dd";
51+
private final TicketUpdater ticketUpdater;
4452

4553
@Transactional(readOnly = true)
4654
public EventTicketInfoResponse getEventTicketInfo(final long eventId, final LocalDateTime now) {
@@ -75,14 +83,14 @@ public EventTicketInfoResponse getEventTicketInfo(final long eventId, final Loca
7583
}
7684

7785
@Transactional(readOnly = true)
78-
public UserBuyTicketInfo getUserBuyTicketInfo(final Long userId) {
86+
public UserBuyTicketInfoResponse getUserBuyTicketInfo(final Long userId) {
7987
if (userId == null) {
80-
return new UserBuyTicketInfo(List.of());
88+
return new UserBuyTicketInfoResponse(List.of());
8189
}
8290
try {
8391
final List<Ticket> ticketList = ticketRetriever.findAllTicketsByUserId(userId);
8492
if (ticketList.isEmpty()) {
85-
return new UserBuyTicketInfo(List.of());
93+
return new UserBuyTicketInfoResponse(List.of());
8694
}
8795

8896
final Map<Long, TicketType> ticketTypeMap = getTicketTypeMap(ticketList);
@@ -93,19 +101,61 @@ public UserBuyTicketInfo getUserBuyTicketInfo(final Long userId) {
93101

94102
final Map<String, BigDecimal> refundAmountByOrderId = findRefundAmountsFromPaymentIfAllTicketsCanceled(TicketListGroupedByOrderIdMap);
95103

96-
final List<UserBuyTicketInfo.Order> orders = convertToOrderList(TicketListGroupedByOrderIdMap, eventMap, ticketTypeMap, refundAmountByOrderId);
104+
final List<UserBuyTicketInfoResponse.Order> orders = convertToOrderList(TicketListGroupedByOrderIdMap, eventMap, ticketTypeMap, refundAmountByOrderId);
97105

98-
return new UserBuyTicketInfo(orders);
106+
return new UserBuyTicketInfoResponse(orders);
99107
} catch (TicketTypeNotfoundException e) {
100108
throw new NotFoundTicketException(ErrorCode.NOT_FOUND_TICKET_TYPE);
101109
}
110+
}
111+
112+
@Transactional
113+
public void confirmTicket(final String ticketCode, final String checkCodeFromTicket) {
114+
try {
115+
final TicketEntity ticketEntity = ticketRetriever.findTicketEntityByTicketCode(ticketCode);
116+
verifyTicketStatus(ticketEntity.getStatus());
117+
118+
final TicketType ticketType = ticketTypeRetriever.findTicketTypeById(ticketEntity.getTicketTypeId());
119+
verifyTicketDate(ticketType.getTicketStartAt(), ticketType.getTicketEndAt());
120+
121+
final Event event = eventRetriever.findEventById(ticketEntity.getEventId());
122+
verifyTicketCheckCode(event.getTicketCheckCode(), checkCodeFromTicket);
123+
124+
ticketUpdater.updateTicketStatus(ticketEntity, TicketStatus.USED);
125+
} catch (TicketNotFoundException e) {
126+
throw new NotFoundTicketException(ErrorCode.NOT_FOUND_TICKET);
127+
} catch (TicketTypeNotfoundException e) {
128+
throw new NotFoundTicketException(ErrorCode.NOT_FOUND_TICKET_TYPE);
129+
} catch (EventNotfoundException e) {
130+
throw new NotFoundTicketException(ErrorCode.NOT_FOUND_EVENT);
131+
}
132+
}
133+
134+
private void verifyTicketStatus(final TicketStatus ticketStatus) {
135+
if(ticketStatus == TicketStatus.USED) {
136+
throw new ConflictTicketException(ErrorCode.CONFLICT_ALREADY_USED_TICKET);
137+
} else if(ticketStatus == TicketStatus.CANCELED) {
138+
throw new IllegalTicketException(ErrorCode.BAD_REQUEST_CANCELED_TICKET);
139+
}
140+
}
102141

142+
private void verifyTicketCheckCode(final String ticketCheckCode, final String checkCodeFromTicket) {
143+
if(!Objects.equals(ticketCheckCode, checkCodeFromTicket)) {
144+
throw new IllegalTicketException(ErrorCode.BAD_REQUEST_TICKET_CHECK_CODE_ERROR);
145+
}
146+
}
147+
148+
private void verifyTicketDate(final LocalDateTime ticketStartAt, final LocalDateTime ticketEndAt) {
149+
final LocalDateTime now = LocalDateTime.now();
150+
if(now.isBefore(ticketStartAt) || now.isAfter(ticketEndAt)) {
151+
throw new DateTicketException(ErrorCode.BAD_REQUEST_DATE_TIME_ERROR);
152+
}
103153
}
104154

105-
private List<UserBuyTicketInfo.Order> convertToOrderList(final Map<String, List<Ticket>> ticketsGroupedByOrderId,
106-
final Map<Long, Event> eventMap,
107-
final Map<Long, TicketType> ticketTypeMap,
108-
final Map<String, BigDecimal> refundAmountByOrderId) {
155+
private List<UserBuyTicketInfoResponse.Order> convertToOrderList(final Map<String, List<Ticket>> ticketsGroupedByOrderId,
156+
final Map<Long, Event> eventMap,
157+
final Map<Long, TicketType> ticketTypeMap,
158+
final Map<String, BigDecimal> refundAmountByOrderId) {
109159
return ticketsGroupedByOrderId.entrySet().stream()
110160
.map(entry -> {
111161
final String orderId = entry.getKey();
@@ -117,12 +167,12 @@ private List<UserBuyTicketInfo.Order> convertToOrderList(final Map<String, List<
117167
final String eventName = eventMap.get(eventId).getName();
118168
final String eventVenue = eventMap.get(eventId).getVenue();
119169

120-
final List<UserBuyTicketInfo.TicketInfo> ticketInfos = ticketsInOrder.stream()
170+
final List<UserBuyTicketInfoResponse.TicketInfo> ticketInfos = ticketsInOrder.stream()
121171
.map(ticket -> {
122172
final TicketType ticketType = ticketTypeMap.get(ticket.getTicketTypeId());
123173
final boolean expired = isTicketDateExpired(ticketType.getTicketEndAt());
124174

125-
return new UserBuyTicketInfo.TicketInfo(
175+
return new UserBuyTicketInfoResponse.TicketInfo(
126176
ticket.getTicketCode(),
127177
ticketType.getTicketTypeName(),
128178
toUiStatus(ticket.getStatus(), expired),
@@ -133,30 +183,30 @@ private List<UserBuyTicketInfo.Order> convertToOrderList(final Map<String, List<
133183

134184
// 한 오더내에서 모든 티켓이 USABLE 상태일 때만 취소 가능함
135185
final boolean canCancel = ticketInfos.stream()
136-
.allMatch(info -> info.ticketStatus() == UserBuyTicketInfo.TicketStatusForUi.USABLE);
186+
.allMatch(info -> info.ticketStatus() == UserBuyTicketInfoResponse.TicketStatusForUi.USABLE);
137187

138188
final BigDecimal refundAmount = refundAmountByOrderId.get(orderId);
139189
final String formattedRefund = refundAmount != null ? PriceFormatterUtil.formatPrice(refundAmount) : null;
140190

141-
return new UserBuyTicketInfo.Order(orderDate, orderId, eventName, eventVenue, formattedRefund,canCancel, ticketInfos);
191+
return new UserBuyTicketInfoResponse.Order(orderDate, orderId, eventName, eventVenue, formattedRefund,canCancel, ticketInfos);
142192
})
143-
.sorted(Comparator.comparing(UserBuyTicketInfo.Order::orderDate).reversed())
193+
.sorted(Comparator.comparing(UserBuyTicketInfoResponse.Order::orderDate).reversed())
144194
.toList();
145195
}
146196

147197
private boolean isTicketDateExpired(final LocalDateTime endDate) {
148198
return LocalDateTime.now().isAfter(endDate);
149199
}
150200

151-
private UserBuyTicketInfo.TicketStatusForUi toUiStatus(final TicketStatus status, final boolean expired) {
201+
private UserBuyTicketInfoResponse.TicketStatusForUi toUiStatus(final TicketStatus status, final boolean expired) {
152202
if (expired && status == TicketStatus.RESERVED) {
153-
return UserBuyTicketInfo.TicketStatusForUi.EXPIRED;
203+
return UserBuyTicketInfoResponse.TicketStatusForUi.EXPIRED;
154204
}
155205

156206
return switch (status) {
157-
case RESERVED -> UserBuyTicketInfo.TicketStatusForUi.USABLE;
158-
case USED -> UserBuyTicketInfo.TicketStatusForUi.USED;
159-
case CANCELED -> UserBuyTicketInfo.TicketStatusForUi.REFUNDED;
207+
case RESERVED -> UserBuyTicketInfoResponse.TicketStatusForUi.USABLE;
208+
case USED -> UserBuyTicketInfoResponse.TicketStatusForUi.USED;
209+
case CANCELED -> UserBuyTicketInfoResponse.TicketStatusForUi.REFUNDED;
160210
};
161211
}
162212

src/main/java/com/permitseoul/permitserver/domain/ticket/core/component/TicketRetriever.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
public class TicketRetriever {
1717
private final TicketRepository ticketRepository;
1818

19+
@Transactional(readOnly = true)
20+
public TicketEntity findTicketEntityByTicketCode(final String ticketCode) {
21+
return ticketRepository.findByTicketCode(ticketCode).orElseThrow(TicketNotFoundException::new);
22+
}
23+
1924
@Transactional(readOnly = true)
2025
public List<Ticket> findAllTicketsByOrderIdAndUserId(final String orderId, final long userId) {
2126
final List<TicketEntity> ticketEntityList = ticketRepository.findAllByOrderIdAndUserId(orderId, userId);

src/main/java/com/permitseoul/permitserver/domain/ticket/core/domain/entity/TicketEntity.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ public static TicketEntity create(final long userId, final String orderId, final
5959

6060
public void updateTicketStatus(final TicketStatus status) {
6161
this.status = status;
62+
if(status == TicketStatus.USED) {
63+
this.usedTime = LocalDateTime.now();
64+
}
6265
}
6366
}
6467

0 commit comments

Comments
 (0)