Skip to content
This repository has been archived by the owner on Nov 7, 2023. It is now read-only.

Commit

Permalink
Refactor: Rewrite and simplify the data model for booking
Browse files Browse the repository at this point in the history
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
nt-gt committed Nov 2, 2023
1 parent 1f55eba commit db1a4b7
Show file tree
Hide file tree
Showing 65 changed files with 1,472 additions and 2,780 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public BookingRequestRefStatusTO cancelBookingRequest(
if (!bookingCancelRequestTO.bookingStatus().equals(BookingStatus.CANCELLED)) {
throw ConcreteRequestErrorMessageException.invalidInput("bookingStatus must be CANCELLED");
}
return bookingRequestService.cancelBookingRequest(carrierBookingRequestReference, bookingCancelRequestTO.reason())
return bookingRequestService.cancelBooking(carrierBookingRequestReference, bookingCancelRequestTO.reason())
.orElseThrow(
() ->
ConcreteRequestErrorMessageException.notFound(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import lombok.RequiredArgsConstructor;
import org.dcsa.edocumentation.service.unofficial.ManageShipmentService;
import org.dcsa.edocumentation.transferobjects.BookingRequestRefStatusTO;
import org.dcsa.edocumentation.transferobjects.unofficial.ConfirmedBookingRefStatusTO;
import org.dcsa.edocumentation.transferobjects.unofficial.ManageConfirmedBookingRequestTO;
import org.springframework.http.HttpStatus;
Expand All @@ -22,7 +23,7 @@ public class ManageConfirmedBookingController {
@PostMapping(
path = "/unofficial${spring.application.bkg-context-path}/confirmed-bookings")
@ResponseStatus(HttpStatus.OK)
public ConfirmedBookingRefStatusTO createNewConfirmedBooking(@Valid @RequestBody ManageConfirmedBookingRequestTO shipmentRequestTO) {
public BookingRequestRefStatusTO createNewConfirmedBooking(@Valid @RequestBody ManageConfirmedBookingRequestTO shipmentRequestTO) {
return manageShipmentService.create(shipmentRequestTO);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
public class AdvanceManifestFiling {
@GeneratedValue
@Id private UUID manifest_id;

@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "confirmed_booking_id")
@JoinColumn(name = "booking_data_id")
@Setter(AccessLevel.PACKAGE)
private ConfirmedBooking confirmedBooking;
private BookingData bookingData;

@Column(name = "manifest_type_code")
private String manifestTypeCode;
Expand Down
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
);
}

}
Loading

0 comments on commit db1a4b7

Please sign in to comment.