This repository has been archived by the owner on Nov 7, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor: Rewrite and simplify the data model for booking
The carrier SMEs have decided that there should be one and only one endpoint for bookings. This means the distinguish between a `BookingRequest` and `ConfirmedBooking` should no longer be entity but only status level changes. To prepare for that, I have rewritten the data model with this focus in mind. In this rewrite, `BookingRequest` and `ConfirmedBooking` are replaced by `Booking` and `BookingData`. The `BookingData` contains the combined set of attributes from `BookingRequest` and `ConfirmedBooking` with a very few "meta data" attributes (such as `carrierBookingRequestReference`, `carrierBookingReference`, and the `bookingStatus`). Everything else is in the `BookingData` entity. Because this is a massive internal rewrite, I made it a goal to keep the postman test passing running without any changes to the postman collection. This should give us confidence that this is purely an internal "implementation-detail"-level rewrite. No externally visible contracts have chanced. Other notes: * Henrik is still working on the updated Swagger specs for the new design. It is currently a private draft at [DRAFT-SWAGGER] * I made *no* changes to TOs or controllers. Again to avoid changing the postman tests. I expect we will able to regnerate those once Henrik is done with the swagger rewrite. * Most of the SQL changes is removing now redundant SQL test data. Most of it became redundant with the removal of the summaries endpoint, but was not removed at the time. I removed it in this commit because the data was not used anyway and in some cases violated "1-1" constraints (we had multiple ConfirmedBookings linking to the BookingRequest, which is no longer possible in the new data model design). * I had to keep some "dead" attributes around (such as the "create" and "update" timestamps) because they are required by a JSON schema in the postman collection. The reference implementation does not maintain these field as strictly as before as they are scheduled for removal (DT-389). Just enough to satisify the JSON schema requirement to avoid having to touch the postman collection. [DRAFT-SWAGGER]: https://app.swaggerhub.com/apis/dccsaorg/DCSA_BKG/2.0.0-Beta-1-Henrik Signed-off-by: Niels Thykier <[email protected]>
- Loading branch information
Showing
65 changed files
with
1,472 additions
and
2,780 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
This file contains 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
280 changes: 280 additions & 0 deletions
280
...ation-domain/src/main/java/org/dcsa/edocumentation/domain/persistence/entity/Booking.java
This file contains 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,280 @@ | ||
package org.dcsa.edocumentation.domain.persistence.entity; | ||
|
||
import static org.dcsa.edocumentation.infra.enums.BookingStatus.*; | ||
|
||
import jakarta.persistence.*; | ||
import jakarta.validation.Validator; | ||
import jakarta.validation.constraints.NotNull; | ||
|
||
import java.time.OffsetDateTime; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.Set; | ||
import java.util.UUID; | ||
import lombok.*; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.dcsa.edocumentation.domain.dfa.AbstractStateMachine; | ||
import org.dcsa.edocumentation.domain.dfa.CannotLeaveTerminalStateException; | ||
import org.dcsa.edocumentation.domain.dfa.DFADefinition; | ||
import org.dcsa.edocumentation.domain.dfa.TargetStateIsNotSuccessorException; | ||
import org.dcsa.edocumentation.domain.persistence.entity.unofficial.ValidationResult; | ||
import org.dcsa.edocumentation.domain.validations.*; | ||
import org.dcsa.edocumentation.infra.enums.BookingStatus; | ||
import org.dcsa.edocumentation.infra.validation.StringEnumValidation; | ||
import org.dcsa.skernel.errors.exceptions.ConcreteRequestErrorMessageException; | ||
import org.springframework.data.annotation.CreatedDate; | ||
import org.springframework.data.annotation.LastModifiedDate; | ||
|
||
@NamedEntityGraph( | ||
name = "graph.booking", | ||
attributeNodes = { | ||
@NamedAttributeNode(value = "bookingData", subgraph = "graph.booking-data"), | ||
}) | ||
@Slf4j | ||
@Data | ||
@EqualsAndHashCode(callSuper = true) | ||
@Builder(toBuilder = true) | ||
@NoArgsConstructor | ||
@AllArgsConstructor | ||
@Setter(AccessLevel.PRIVATE) | ||
@Entity | ||
@Table(name = "booking") | ||
public class Booking extends AbstractStateMachine<String> { | ||
|
||
private static final Set<String> CAN_BE_VALIDATED = Set.of(RECEIVED, | ||
PENDING_UPDATES_CONFIRMATION, | ||
PENDING_AMENDMENTS_APPROVAL); | ||
|
||
private static final DFADefinition<String> BOOKING_DFA_DEFINITION = DFADefinition.builder(RECEIVED) | ||
.nonTerminalState(RECEIVED) | ||
.successorNodes(CONFIRMED, PENDING_UPDATE, REJECTED, // Carrier | ||
CANCELLED, PENDING_UPDATES_CONFIRMATION) // Shipper | ||
.nonTerminalState(PENDING_UPDATE) | ||
.successorNodes(PENDING_UPDATE, REJECTED, // Carrier | ||
PENDING_UPDATES_CONFIRMATION, CANCELLED) // Shipper | ||
.nonTerminalState(PENDING_UPDATES_CONFIRMATION) | ||
.successorNodes(CONFIRMED, PENDING_UPDATE, REJECTED, // Carrier | ||
CANCELLED) // Shipper | ||
.nonTerminalState(CONFIRMED) | ||
.successorNodes(PENDING_UPDATE, COMPLETED, DECLINED, // Carrier | ||
PENDING_AMENDMENTS_APPROVAL, CANCELLED) // Shipper | ||
.nonTerminalState(PENDING_AMENDMENTS_APPROVAL) | ||
.successorNodes(PENDING_UPDATE, CONFIRMED, DECLINED, // Carrier | ||
CANCELLED) // Shipper | ||
.terminalStates(COMPLETED, REJECTED, DECLINED, // Carrier | ||
CANCELLED) // Shipper | ||
.build(); | ||
|
||
@Id | ||
@Column(name = "id", nullable = false) | ||
@GeneratedValue | ||
private UUID id; | ||
|
||
@Column(name = "carrier_booking_request_reference", length = 100) | ||
private String carrierBookingRequestReference; | ||
|
||
@Column(name = "carrier_booking_reference", length = 35) | ||
private String carrierBookingReference; | ||
|
||
@Column(name = "booking_status") | ||
@StringEnumValidation(value = BookingStatus.class) | ||
private String bookingStatus; | ||
|
||
@ToString.Exclude | ||
@EqualsAndHashCode.Exclude | ||
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true, optional = false) | ||
@JoinColumn(name = "booking_data_id") | ||
@Setter(AccessLevel.PACKAGE) | ||
private BookingData bookingData; | ||
|
||
@ToString.Exclude | ||
@EqualsAndHashCode.Exclude | ||
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) | ||
@JoinColumn(name = "last_confirmed_booking_data_id") | ||
@Setter(AccessLevel.PACKAGE) | ||
private BookingData lastConfirmedBookingData; | ||
|
||
@ToString.Exclude | ||
@EqualsAndHashCode.Exclude | ||
@OneToMany(cascade = CascadeType.ALL) | ||
@JoinColumn(name = "booking_id", referencedColumnName = "id", nullable = false) | ||
@OrderColumn(name = "element_order") | ||
private List<BookingRequestedChange> requestedChanges; | ||
|
||
// TODO: Remove (but we do not want any test changes and test schema currently aspects this) | ||
@CreatedDate | ||
@Column(name = "created_date_time") | ||
@Builder.Default | ||
protected OffsetDateTime bookingRequestCreatedDateTime = OffsetDateTime.now(); | ||
|
||
// TODO: Remove (but we do not want any test changes and test schema currently aspects this) | ||
@LastModifiedDate | ||
@Column(name = "updated_date_time") | ||
@Builder.Default | ||
protected OffsetDateTime bookingRequestUpdatedDateTime = OffsetDateTime.now(); | ||
|
||
public void assignCarrierBookingReference(@NotNull String carrierBookingReference) { | ||
if (this.carrierBookingReference != null | ||
&& !this.carrierBookingReference.equals(carrierBookingReference)) { | ||
|
||
} | ||
this.carrierBookingReference = carrierBookingReference; | ||
} | ||
|
||
/** | ||
* Subject to change. Reefer will probably change it. | ||
*/ | ||
public ValidationResult<String> asyncValidation(Validator validator) { | ||
List<String> validationErrors = new ArrayList<>(); | ||
|
||
if (!CAN_BE_VALIDATED.contains(bookingStatus)) { | ||
throw new IllegalStateException("bookingStatus must be one of " + CAN_BE_VALIDATED); | ||
} | ||
if (this.requestedChanges == null) { | ||
this.requestedChanges = new ArrayList<>(); | ||
} | ||
clearRequestedChanges(); | ||
|
||
for (var violation : validator.validate(this.bookingData, AsyncShipperProvidedDataValidation.class)) { | ||
this.requestedChanges.add(BookingRequestedChange.fromConstraintViolation(violation)); | ||
validationErrors.add(violation.getPropertyPath().toString() + ": " + violation.getMessage()); | ||
} | ||
|
||
// TODO: according to the latest Booking State Transition Diagram (STD), | ||
// PENDING_UPDATES_CONFIRMATION should be replaced with CONFIRMED, but this change should be done together | ||
// with other STD-related changes so that new BOOKING_DFA_DEFINITION does not get broken | ||
var proposedStatus = validationErrors.isEmpty() ? PENDING_UPDATES_CONFIRMATION : PENDING_UPDATE; | ||
|
||
return new ValidationResult<>(proposedStatus, validationErrors); | ||
} | ||
|
||
private void clearRequestedChanges() { | ||
if (this.requestedChanges != null && !this.requestedChanges.isEmpty()) { | ||
this.requestedChanges.clear(); | ||
} | ||
} | ||
|
||
|
||
/** | ||
* Transition the booking into its {@link BookingStatus#RECEIVED} state. | ||
*/ | ||
public void receive() { | ||
processTransition(RECEIVED, null, false); | ||
} | ||
|
||
/** | ||
* Transition the booking into its {@link BookingStatus#CANCELLED} state. | ||
*/ | ||
public void cancel(String reason) { | ||
processTransition(CANCELLED, reason, false); | ||
} | ||
|
||
/** | ||
* Transition the booking into its {@link BookingStatus#REJECTED} state. | ||
*/ | ||
public void reject(String reason) { | ||
processTransition(REJECTED, reason, false); | ||
} | ||
|
||
/** | ||
* Transition the booking into its {@link BookingStatus#REJECTED} state. | ||
*/ | ||
public void decline(String reason) { | ||
processTransition(DECLINED, reason, false); | ||
} | ||
|
||
/** | ||
* Transition the booking into its {@link BookingStatus#PENDING_UPDATE} state. | ||
*/ | ||
public void pendingUpdate(String reason) { | ||
processTransition(PENDING_UPDATE, reason, false); | ||
} | ||
|
||
/** | ||
* Transition the booking into its {@link BookingStatus#PENDING_UPDATES_CONFIRMATION} state | ||
* as a consequence of a shipper provided change | ||
*/ | ||
public void pendingUpdatesConfirmation(@NotNull BookingData newBookingData) { | ||
this.pendingUpdatesConfirmation(); | ||
this.bookingData = newBookingData; | ||
} | ||
|
||
/** | ||
* Transition the booking into its {@link BookingStatus#PENDING_UPDATES_CONFIRMATION} state | ||
* as a consequence of a carrier side (partial) validation. | ||
*/ | ||
public void pendingUpdatesConfirmation() { | ||
processTransition(PENDING_UPDATES_CONFIRMATION, null, true); | ||
} | ||
|
||
/** | ||
* Transition the booking into its {@link BookingStatus#PENDING_AMENDMENTS_APPROVAL} state. | ||
*/ | ||
public void pendingAmendmentsApproval(String reason) { | ||
processTransition(PENDING_AMENDMENTS_APPROVAL, reason, true); | ||
} | ||
|
||
/** | ||
* Transition the booking into its {@link BookingStatus#CONFIRMED} state. | ||
*/ | ||
public void confirm() { | ||
// TODO: Validate that all carrier provided attributes for confirming the booking has been given | ||
processTransition(CONFIRMED, null, true); | ||
this.lastConfirmedBookingData = this.bookingData; | ||
} | ||
|
||
/** | ||
* Transition the booking into its {@link BookingStatus#COMPLETED} state. | ||
*/ | ||
public void complete() { | ||
processTransition(COMPLETED, null, true); | ||
} | ||
|
||
@Override | ||
protected DFADefinition<String> getDfaDefinition() { | ||
return BOOKING_DFA_DEFINITION; | ||
} | ||
|
||
@Override | ||
protected String getResumeFromState() { | ||
return this.bookingStatus; | ||
} | ||
|
||
protected void processTransition(String bookingStatus, String reason, boolean clearRequestedChanges) { | ||
transitionTo(bookingStatus); | ||
this.bookingStatus = bookingStatus; | ||
if (carrierBookingRequestReference == null) { | ||
carrierBookingRequestReference = UUID.randomUUID().toString(); | ||
} | ||
if (clearRequestedChanges) { | ||
this.clearRequestedChanges(); | ||
} | ||
} | ||
|
||
@Override | ||
protected RuntimeException errorForAttemptToLeaveTerminalState(String currentState, | ||
String successorState, | ||
CannotLeaveTerminalStateException e) { | ||
log.error("Booking with id=" + (id != null ? id.toString() : "null") +" is in terminal state " + currentState + | ||
", can not transition to state " + successorState); | ||
return ConcreteRequestErrorMessageException.conflict( | ||
"Cannot perform the requested action on the booking because the booking status is '" | ||
+ currentState + "'", | ||
e | ||
); | ||
} | ||
|
||
@Override | ||
protected RuntimeException errorForTargetStateNotListedAsSuccessor(String currentState, | ||
String successorState, | ||
TargetStateIsNotSuccessorException e) { | ||
log.error("Booking with id=" + (id != null ? id.toString() : "null") +" is in state " + currentState + | ||
", can not transition to unexpected state " + successorState); | ||
return ConcreteRequestErrorMessageException.conflict( | ||
"It is not possible to perform the requested action on the booking with the booking status '" | ||
+ currentState + "'", | ||
e | ||
); | ||
} | ||
|
||
} |
Oops, something went wrong.