-
Notifications
You must be signed in to change notification settings - Fork 1
Implement feature booking #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ducls-2223
wants to merge
3
commits into
awesome-academy:master
Choose a base branch
from
ducls-2223:feat/bookings
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
12 changes: 12 additions & 0 deletions
12
src/main/java/com/tripgoapi/application/port/in/CancelBookingUseCase.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.tripgoapi.application.port.in; | ||
|
|
||
| import com.tripgoapi.domain.model.Booking; | ||
|
|
||
| public interface CancelBookingUseCase { | ||
|
|
||
| /** | ||
| * @param requesterId id of the currently authenticated user; enforces that only the | ||
| * booking's owner may cancel it | ||
| */ | ||
| Booking cancelBooking(Long bookingId, Long requesterId); | ||
| } |
16 changes: 16 additions & 0 deletions
16
src/main/java/com/tripgoapi/application/port/in/CreateBookingCommand.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package com.tripgoapi.application.port.in; | ||
|
|
||
| import java.time.LocalDate; | ||
|
|
||
| public record CreateBookingCommand( | ||
| String idempotencyKey, | ||
| Long userId, | ||
| Long tourId, | ||
| LocalDate date, | ||
| int adults, | ||
| int children, | ||
| String contactName, | ||
| String contactEmail, | ||
| String contactPhone | ||
| ) { | ||
| } |
7 changes: 7 additions & 0 deletions
7
src/main/java/com/tripgoapi/application/port/in/CreateBookingUseCase.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.tripgoapi.application.port.in; | ||
|
|
||
| import com.tripgoapi.domain.model.Booking; | ||
|
|
||
| public interface CreateBookingUseCase { | ||
| Booking createBooking(CreateBookingCommand command); | ||
| } |
12 changes: 12 additions & 0 deletions
12
src/main/java/com/tripgoapi/application/port/in/GetBookingDetailUseCase.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.tripgoapi.application.port.in; | ||
|
|
||
| import com.tripgoapi.domain.model.Booking; | ||
|
|
||
| public interface GetBookingDetailUseCase { | ||
|
|
||
| /** | ||
| * @param requesterId id of the currently authenticated user; enforces that only the | ||
| * booking's owner may view it | ||
| */ | ||
| Booking getBookingDetail(Long bookingId, Long requesterId); | ||
| } |
14 changes: 14 additions & 0 deletions
14
src/main/java/com/tripgoapi/application/port/in/GetBookingsUseCase.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.tripgoapi.application.port.in; | ||
|
|
||
| import com.tripgoapi.domain.model.Booking; | ||
| import com.tripgoapi.domain.model.BookingStatus; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public interface GetBookingsUseCase { | ||
|
|
||
| /** | ||
| * @param status optional filter; {@code null} returns bookings in any status | ||
| */ | ||
| List<Booking> getBookingsForUser(Long userId, BookingStatus status); | ||
| } |
46 changes: 46 additions & 0 deletions
46
src/main/java/com/tripgoapi/application/port/out/BookingRepositoryInterface.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package com.tripgoapi.application.port.out; | ||
|
|
||
| import com.tripgoapi.domain.model.Booking; | ||
| import com.tripgoapi.domain.model.BookingStatus; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Optional; | ||
|
|
||
| public interface BookingRepositoryInterface { | ||
|
|
||
| /** | ||
| * Persists a new booking. The given {@code booking} has no {@code id}/{@code bookingCode} | ||
| * yet; the returned instance has both populated. | ||
| */ | ||
| Booking save(Booking booking); | ||
|
|
||
| /** | ||
| * @param status optional filter; {@code null} returns bookings in any status | ||
| */ | ||
| List<Booking> findByUserId(Long userId, BookingStatus status); | ||
|
|
||
| /** | ||
| * Idempotency keys are client-generated and only unique per submitting user — never look | ||
| * this up without also constraining by userId, or one user's retried request could return | ||
| * another user's booking. | ||
| */ | ||
| Optional<Booking> findByUserIdAndIdempotencyKey(Long userId, String idempotencyKey); | ||
|
|
||
| /** | ||
| * Same lookup as {@link #findByUserIdAndIdempotencyKey}, but always runs in a brand new | ||
| * transaction. Needed to recover after a failed {@link #save} caused by a concurrent request | ||
| * winning the same (userId, idempotencyKey) race: Postgres aborts the whole transaction on a | ||
| * unique-constraint violation, so no further statement can run in the caller's transaction | ||
| * until it rolls back. | ||
| */ | ||
| Optional<Booking> findByUserIdAndIdempotencyKeyInNewTransaction(Long userId, String idempotencyKey); | ||
|
|
||
| Optional<Booking> findById(Long id); | ||
|
|
||
| /** | ||
| * Atomically transitions PENDING/CONFIRMED -> CANCELLED in a single statement, so two | ||
| * concurrent cancel requests for the same booking can't both succeed and double-release slots. | ||
| * @return true if the transition happened (booking existed and was in a cancellable state) | ||
| */ | ||
| boolean cancelIfCancellable(Long id); | ||
| } |
21 changes: 21 additions & 0 deletions
21
src/main/java/com/tripgoapi/application/port/out/TourDepartureRepositoryInterface.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package com.tripgoapi.application.port.out; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.util.Optional; | ||
|
|
||
| public interface TourDepartureRepositoryInterface { | ||
|
|
||
| Optional<Long> findDepartureId(Long tourId, LocalDate departureDate); | ||
|
|
||
| /** | ||
| * Atomically reserves {@code guestCount} slots on the given departure. | ||
| * @return true if there was enough remaining capacity and the reservation succeeded | ||
| */ | ||
| boolean reserveSlots(Long departureId, int guestCount); | ||
|
|
||
| /** | ||
| * Atomically releases {@code guestCount} previously-reserved slots back to the departure | ||
| * (e.g. on booking cancellation). | ||
| */ | ||
| void releaseSlots(Long departureId, int guestCount); | ||
| } |
50 changes: 50 additions & 0 deletions
50
src/main/java/com/tripgoapi/application/service/CancelBookingService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package com.tripgoapi.application.service; | ||
|
|
||
| import com.tripgoapi.application.port.in.CancelBookingUseCase; | ||
| import com.tripgoapi.application.port.out.BookingRepositoryInterface; | ||
| import com.tripgoapi.application.port.out.TourDepartureRepositoryInterface; | ||
| import com.tripgoapi.domain.exception.BookingAccessDeniedException; | ||
| import com.tripgoapi.domain.exception.BookingCancellationNotAllowedException; | ||
| import com.tripgoapi.domain.exception.BookingNotFoundException; | ||
| import com.tripgoapi.domain.model.Booking; | ||
| import com.tripgoapi.domain.model.BookingStatus; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class CancelBookingService implements CancelBookingUseCase { | ||
|
|
||
| private final BookingRepositoryInterface bookingRepository; | ||
| private final TourDepartureRepositoryInterface tourDepartureRepository; | ||
|
|
||
| @Override | ||
| @Transactional | ||
| public Booking cancelBooking(Long bookingId, Long requesterId) { | ||
| Booking booking = bookingRepository.findById(bookingId) | ||
| .orElseThrow(() -> new BookingNotFoundException(bookingId)); | ||
|
|
||
| if (!booking.userId().equals(requesterId)) { | ||
| throw new BookingAccessDeniedException(bookingId); | ||
| } | ||
|
|
||
| // Atomic guard: only one of several concurrent cancel requests for this booking can | ||
| // flip PENDING/CONFIRMED -> CANCELLED; a losing request gets false here instead of both | ||
| // passing a stale in-memory status check and double-releasing slots. | ||
| if (!bookingRepository.cancelIfCancellable(bookingId)) { | ||
| // Re-read rather than reuse `booking.status()`: that snapshot was taken before | ||
| // cancelIfCancellable ran, so for a request that lost the race it would still say | ||
| // PENDING/CONFIRMED even though the real, current status is CANCELLED/COMPLETED. | ||
| BookingStatus currentStatus = bookingRepository.findById(bookingId) | ||
| .orElseThrow(() -> new BookingNotFoundException(bookingId)) | ||
| .status(); | ||
| throw new BookingCancellationNotAllowedException(currentStatus); | ||
| } | ||
|
|
||
| tourDepartureRepository.releaseSlots(booking.departureId(), booking.adults() + booking.children()); | ||
|
|
||
| return bookingRepository.findById(bookingId) | ||
| .orElseThrow(() -> new BookingNotFoundException(bookingId)); | ||
| } | ||
| } |
94 changes: 94 additions & 0 deletions
94
src/main/java/com/tripgoapi/application/service/CreateBookingService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| package com.tripgoapi.application.service; | ||
|
|
||
| import com.tripgoapi.application.port.in.CreateBookingCommand; | ||
| import com.tripgoapi.application.port.in.CreateBookingUseCase; | ||
| import com.tripgoapi.application.port.out.BookingRepositoryInterface; | ||
| import com.tripgoapi.application.port.out.TourDepartureRepositoryInterface; | ||
| import com.tripgoapi.application.port.out.TourDetailRepositoryInterface; | ||
| import com.tripgoapi.domain.exception.BookingGroupTooLargeException; | ||
| import com.tripgoapi.domain.exception.NoAvailableSlotsException; | ||
| import com.tripgoapi.domain.exception.TourDepartureNotFoundException; | ||
| import com.tripgoapi.domain.exception.TourNotFoundException; | ||
| import com.tripgoapi.domain.model.Booking; | ||
| import com.tripgoapi.domain.model.TourDetail; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.dao.DataIntegrityViolationException; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
| import org.springframework.transaction.interceptor.TransactionAspectSupport; | ||
| import org.springframework.transaction.support.TransactionSynchronizationManager; | ||
|
|
||
| import java.math.BigDecimal; | ||
| import java.util.Optional; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class CreateBookingService implements CreateBookingUseCase { | ||
|
|
||
| private final TourDetailRepositoryInterface tourDetailRepository; | ||
| private final TourDepartureRepositoryInterface tourDepartureRepository; | ||
| private final BookingRepositoryInterface bookingRepository; | ||
|
|
||
| @Override | ||
| @Transactional | ||
| public Booking createBooking(CreateBookingCommand command) { | ||
| // Idempotency: a retried request (client timeout/double-submit) with the same key | ||
| // returns the already-created booking instead of reserving slots a second time. Scoped | ||
| // to the caller's userId — the key is client-generated and only unique per user, so an | ||
| // unscoped lookup would let user B fetch user A's booking by guessing/colliding a key. | ||
| Optional<Booking> existing = bookingRepository.findByUserIdAndIdempotencyKey(command.userId(), command.idempotencyKey()); | ||
| if (existing.isPresent()) { | ||
| return existing.get(); | ||
| } | ||
|
|
||
| TourDetail tour = tourDetailRepository.findById(command.tourId()) | ||
| .orElseThrow(() -> new TourNotFoundException(command.tourId())); | ||
|
|
||
| Long departureId = tourDepartureRepository.findDepartureId(command.tourId(), command.date()) | ||
| .orElseThrow(() -> new TourDepartureNotFoundException(command.tourId(), command.date())); | ||
|
|
||
| int guestCount = command.adults() + command.children(); | ||
| if (tour.maxGuests() != null && guestCount > tour.maxGuests()) { | ||
| throw new BookingGroupTooLargeException(guestCount, tour.maxGuests()); | ||
| } | ||
|
|
||
| if (!tourDepartureRepository.reserveSlots(departureId, guestCount)) { | ||
| throw new NoAvailableSlotsException(); | ||
| } | ||
|
|
||
| BigDecimal unitPrice = tour.discountPrice() != null ? tour.discountPrice() : tour.price(); | ||
| BigDecimal totalPrice = unitPrice.multiply(BigDecimal.valueOf(guestCount)); | ||
|
|
||
| Booking booking = Booking.pending( | ||
| command.idempotencyKey(), | ||
| command.userId(), | ||
| tour, | ||
| departureId, | ||
| command.date(), | ||
| command.adults(), | ||
| command.children(), | ||
| totalPrice, | ||
| command.contactName(), | ||
| command.contactEmail(), | ||
| command.contactPhone() | ||
| ); | ||
|
|
||
| try { | ||
| return bookingRepository.save(booking); | ||
| } catch (DataIntegrityViolationException ex) { | ||
| // Lost a double-submit race: a concurrent request with the same (userId, | ||
| // idempotencyKey) committed first, and Postgres aborted this transaction on the | ||
| // unique-constraint violation — no further statement can run in it. Mark it | ||
| // rollback-only (releasing the slots this losing request reserved) and hand the | ||
| // client the winner's booking instead of a raw 500, reading it in a fresh transaction | ||
| // since this one is now unusable. Guarded: only active when this method actually runs | ||
| // behind the @Transactional proxy (always true in production; not the case when a | ||
| // unit test calls the service directly with no Spring context). | ||
| if (TransactionSynchronizationManager.isActualTransactionActive()) { | ||
| TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); | ||
| } | ||
| return bookingRepository.findByUserIdAndIdempotencyKeyInNewTransaction(command.userId(), command.idempotencyKey()) | ||
| .orElseThrow(() -> ex); | ||
| } | ||
| } | ||
| } | ||
30 changes: 30 additions & 0 deletions
30
src/main/java/com/tripgoapi/application/service/GetBookingDetailService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package com.tripgoapi.application.service; | ||
|
|
||
| import com.tripgoapi.application.port.in.GetBookingDetailUseCase; | ||
| import com.tripgoapi.application.port.out.BookingRepositoryInterface; | ||
| import com.tripgoapi.domain.exception.BookingAccessDeniedException; | ||
| import com.tripgoapi.domain.exception.BookingNotFoundException; | ||
| import com.tripgoapi.domain.model.Booking; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class GetBookingDetailService implements GetBookingDetailUseCase { | ||
|
|
||
| private final BookingRepositoryInterface bookingRepository; | ||
|
|
||
| @Override | ||
| @Transactional(readOnly = true) | ||
| public Booking getBookingDetail(Long bookingId, Long requesterId) { | ||
| Booking booking = bookingRepository.findById(bookingId) | ||
| .orElseThrow(() -> new BookingNotFoundException(bookingId)); | ||
|
|
||
| if (!booking.userId().equals(requesterId)) { | ||
| throw new BookingAccessDeniedException(bookingId); | ||
| } | ||
|
|
||
| return booking; | ||
| } | ||
| } |
24 changes: 24 additions & 0 deletions
24
src/main/java/com/tripgoapi/application/service/GetBookingsService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package com.tripgoapi.application.service; | ||
|
|
||
| import com.tripgoapi.application.port.in.GetBookingsUseCase; | ||
| import com.tripgoapi.application.port.out.BookingRepositoryInterface; | ||
| import com.tripgoapi.domain.model.Booking; | ||
| import com.tripgoapi.domain.model.BookingStatus; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class GetBookingsService implements GetBookingsUseCase { | ||
|
|
||
| private final BookingRepositoryInterface bookingRepository; | ||
|
|
||
| @Override | ||
| @Transactional(readOnly = true) | ||
| public List<Booking> getBookingsForUser(Long userId, BookingStatus status) { | ||
| return bookingRepository.findByUserId(userId, status); | ||
| } | ||
| } |
8 changes: 8 additions & 0 deletions
8
src/main/java/com/tripgoapi/domain/exception/BookingAccessDeniedException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.tripgoapi.domain.exception; | ||
|
|
||
| public class BookingAccessDeniedException extends ForbiddenException { | ||
|
|
||
| public BookingAccessDeniedException(Long bookingId) { | ||
| super("Bạn không có quyền truy cập đơn đặt tour: id=" + bookingId); | ||
| } | ||
| } |
10 changes: 10 additions & 0 deletions
10
src/main/java/com/tripgoapi/domain/exception/BookingCancellationNotAllowedException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.tripgoapi.domain.exception; | ||
|
|
||
| import com.tripgoapi.domain.model.BookingStatus; | ||
|
|
||
| public class BookingCancellationNotAllowedException extends ConflictException { | ||
|
|
||
| public BookingCancellationNotAllowedException(BookingStatus currentStatus) { | ||
| super("Không thể hủy đơn ở trạng thái: " + currentStatus); | ||
| } | ||
| } |
8 changes: 8 additions & 0 deletions
8
src/main/java/com/tripgoapi/domain/exception/BookingGroupTooLargeException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.tripgoapi.domain.exception; | ||
|
|
||
| public class BookingGroupTooLargeException extends ConflictException { | ||
|
|
||
| public BookingGroupTooLargeException(int guestCount, int maxGuests) { | ||
| super("Số khách (" + guestCount + ") vượt quá giới hạn " + maxGuests + " khách/đơn của tour"); | ||
| } | ||
| } |
8 changes: 8 additions & 0 deletions
8
src/main/java/com/tripgoapi/domain/exception/BookingNotFoundException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.tripgoapi.domain.exception; | ||
|
|
||
| public class BookingNotFoundException extends NotFoundException { | ||
|
|
||
| public BookingNotFoundException(Long id) { | ||
| super("Booking not found: id=" + id); | ||
| } | ||
| } |
8 changes: 8 additions & 0 deletions
8
src/main/java/com/tripgoapi/domain/exception/ForbiddenException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.tripgoapi.domain.exception; | ||
|
|
||
| public abstract class ForbiddenException extends RuntimeException { | ||
|
|
||
| protected ForbiddenException(String message) { | ||
| super(message); | ||
| } | ||
| } |
8 changes: 8 additions & 0 deletions
8
src/main/java/com/tripgoapi/domain/exception/InvalidBookingStatusException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.tripgoapi.domain.exception; | ||
|
|
||
| public class InvalidBookingStatusException extends UnprocessableException { | ||
|
|
||
| public InvalidBookingStatusException(String status) { | ||
| super("status không hợp lệ: " + status); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Blocker — rò rỉ đơn hàng giữa các user (IDOR).
findByIdempotencyKeyđang tra cứu toàn cục, không gắn vớicommand.userId(), và booking tìm được được trả thẳng về mà không kiểm tra chủ sở hữu. Key này do client tự sinh và chỉ bị ràng buộc@NotBlank, nên chỉ cần user B gửi một key trùng với user A (vd"1","booking-1") là nhận nguyên đơn của A:bookingCode,totalPrice,contactName/contactEmail/contactPhone. Unique index toàn cục ở V16 làm điều này thành hành vi chắc chắn xảy ra chứ không phải hi hữu. Nên đổi sang tra cứu theo cặp(userId, idempotencyKey):