Skip to content
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

Rewrite RecalculatePriceHandler #609

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,39 @@
import static cds.gen.travelservice.TravelService_.BOOKING;
import static cds.gen.travelservice.TravelService_.BOOKING_SUPPLEMENT;
import static cds.gen.travelservice.TravelService_.TRAVEL;
import static com.sap.cds.ql.CQL.sum;
import static com.sap.cds.services.cds.CqnService.EVENT_CREATE;
import static com.sap.cds.services.cds.CqnService.EVENT_UPDATE;
import static com.sap.cds.services.draft.DraftService.EVENT_DRAFT_CANCEL;
import static com.sap.cds.services.draft.DraftService.EVENT_DRAFT_PATCH;
import static java.lang.Boolean.FALSE;
import static java.util.Objects.requireNonNullElse;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.UnaryOperator;

import org.springframework.stereotype.Component;

import com.sap.cds.Row;
import com.sap.cds.Result;
import com.sap.cds.Struct;
import com.sap.cds.ql.Select;
import com.sap.cds.ql.Update;
import com.sap.cds.ql.cqn.CqnAnalyzer;
import com.sap.cds.ql.cqn.CqnSelect;
import com.sap.cds.ql.cqn.CqnUpdate;
import com.sap.cds.services.ErrorStatuses;
import com.sap.cds.services.ServiceException;
import com.sap.cds.services.cds.CqnService;
import com.sap.cds.services.draft.DraftCancelEventContext;
import com.sap.cds.services.draft.DraftPatchEventContext;
import com.sap.cds.services.draft.DraftService;
import com.sap.cds.services.handler.EventHandler;
import com.sap.cds.services.handler.annotations.After;
import com.sap.cds.services.handler.annotations.Before;
import com.sap.cds.services.handler.annotations.On;
import com.sap.cds.services.handler.annotations.ServiceName;
import com.sap.cds.services.persistence.PersistenceService;
import com.sap.cds.services.utils.StringUtils;

import cds.gen.sap.fe.cap.travel.TravelModel_;
import cds.gen.travelservice.Booking;
import cds.gen.travelservice.BookingSupplement;
import cds.gen.travelservice.BookingSupplement_;
Expand All @@ -41,6 +48,8 @@
@ServiceName(TravelService_.CDS_NAME)
public class RecalculatePriceHandler implements EventHandler {

private static final BigDecimal ZERO = new BigDecimal(0);

private final DraftService draftService;
private final PersistenceService persistenceService;

Expand All @@ -49,112 +58,168 @@ public RecalculatePriceHandler(DraftService draftService, PersistenceService per
this.persistenceService = persistenceService;
}

@Before(event = {CqnService.EVENT_CREATE, CqnService.EVENT_UPDATE}, entity = {Booking_.CDS_NAME, BookingSupplement_.CDS_NAME})
public void disableUpdateAndCreateForBookingAndBookingSupplement() {
throw new ServiceException(ErrorStatuses.BAD_REQUEST, "error.booking.only_patch");
@After(event = { EVENT_CREATE, EVENT_UPDATE }, entity = Travel_.CDS_NAME)
public void calculateTotalPriceOfTravel(Travel travel) {
String travelUUID = travel.travelUUID();
if (travelUUID != null) {
BigDecimal totalPrice = calculateTravelPrice(travelUUID);
persistenceService.run(Update.entity(TRAVEL).data(Travel.create()
.travelUUID(travelUUID)
.totalPrice(totalPrice)));

travel.totalPrice(totalPrice);
}
}

private BigDecimal calculateTravelPrice(String travelUUID) {
BigDecimal bookingFee = run(Select.from(TravelModel_.TRAVEL)
.columns(t -> t.BookingFee().as("sum"))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

booking fee is not a sum

.byId(travelUUID));

BigDecimal flights = run(Select.from(BOOKING)
.columns(b -> b.FlightPrice().sum().as("sum"))
.where(b -> b.to_Travel_TravelUUID().eq(travelUUID)));

BigDecimal supplements = run(Select.from(BOOKING_SUPPLEMENT)
.columns(s -> s.Price().sum().as("sum"))
.where(s -> s.to_Booking().to_Travel().TravelUUID().eq(travelUUID)));

return bookingFee.add(flights).add(supplements);
}

private static BigDecimal calculateTotalPriceForTravel(CqnService db, String travelUUID,
boolean isActiveEntity) {
// get booking fee
BigDecimal bookingFee = BigDecimal.valueOf(0);
Optional<Row> bookingFeeRow = db
.run(Select.from(TRAVEL).columns(Travel_::BookingFee)
.where(t -> t.TravelUUID().eq(travelUUID)
.and(t.IsActiveEntity().eq(isActiveEntity))
.and(t.BookingFee().isNotNull()))
.limit(1))
.first();
if (bookingFeeRow.isPresent()) {
bookingFee = (BigDecimal) bookingFeeRow.get().get("BookingFee");
private BigDecimal run(CqnSelect query) {
Result result = persistenceService.run(query);
BigDecimal sum = result.first(Price.class).map(Price::sum).orElse(ZERO);

return nullToZero(sum);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needed?

}

private interface Price {
BigDecimal sum();
}

@On(event = { EVENT_DRAFT_PATCH }, entity = Travel_.CDS_NAME)
public void updateTravelPriceOnBookingFeeUpdate(DraftPatchEventContext context) {
CqnUpdate update = context.getCqn();
Travel travel = Struct.access(update.data()).as(Travel.class);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Travel travel = Struct.access(update.data()).as(Travel.class);
Travel travelData = Struct.access(update.data()).as(Travel.class);

BigDecimal newFee = travel.bookingFee();
if (newFee != null) {
var travelKeys = CqnAnalyzer.create(context.getModel()).analyze(update).targetKeys();
BigDecimal oldFee = selectTravelFee(travelKeys);
BigDecimal travelPrice = selectTravelPrice(travelKeys.get(Travel.TRAVEL_UUID)).add(newFee).subtract(oldFee);

travel.totalPrice(travelPrice);
}
}

// get sum of flight prices from all bookings
BigDecimal flightPriceSum = new BigDecimal(0);
Optional<Row> flightPriceRow = db
.run(Select.from(BOOKING).columns(b -> sum(b.FlightPrice()).as("FlightPriceSum"))
.where(b -> b.to_Travel_TravelUUID().eq(travelUUID).and(b.IsActiveEntity().eq(isActiveEntity))))
.first();
@On(event = { EVENT_DRAFT_PATCH }, entity = Booking_.CDS_NAME)
public void updateTravelPriceOnBookingUpdate(Booking bookingPatch) {
BigDecimal newPrice = bookingPatch.flightPrice();
if (newPrice != null) {
Booking booking = selectBookingPrice(Booking.create()
.bookingUUID(bookingPatch.bookingUUID())
.isActiveEntity(false));

if (flightPriceRow.isPresent()) {
flightPriceSum = (BigDecimal) Objects.requireNonNullElse(flightPriceRow.get().get("FlightPriceSum"), new BigDecimal(0));
String travelUUID = booking.toTravelTravelUUID();
updateTravelPrice(travelUUID, newPrice, booking.flightPrice());
}
}

@On(event = { EVENT_DRAFT_PATCH }, entity = BookingSupplement_.CDS_NAME)
public void updateTravelPriceOnSupplementUpdate(BookingSupplement supplementPatch) {
BigDecimal newPrice = supplementPatch.price();
if (newPrice != null) {
BookingSupplement supplement = selectSupplementPrice(s -> s
.bookSupplUUID(supplementPatch.bookSupplUUID())
.isActiveEntity(false));

// get sum of the prices of all booking supplements for the travel
BigDecimal supplementPriceSum = new BigDecimal(0);
Optional<Row> supplementPriceSumRow = db
.run(Select.from(BOOKING_SUPPLEMENT).columns(c -> sum(c.Price()).as("PriceSum"))
.where(b -> b.to_Travel_TravelUUID().eq(travelUUID).and(b.IsActiveEntity().eq(isActiveEntity))))
.first();
if (supplementPriceSumRow.isPresent()) {
supplementPriceSum = (BigDecimal) Objects.requireNonNullElse(flightPriceRow.get().get("PriceSum"), new BigDecimal(0));
String travelUUID = supplement.toTravelTravelUUID();
updateTravelPrice(travelUUID, newPrice, supplement.price());
}
}

// update travel's total price
return bookingFee.add(flightPriceSum).add(supplementPriceSum);
private void updateTravelPrice(String travelUUID, BigDecimal newPrice, BigDecimal oldPrice) {
BigDecimal travelPrice = selectTravelPrice(travelUUID).add(newPrice).subtract(nullToZero(oldPrice));
updateTravelPrice(travelUUID, travelPrice);
}

@After(event = {CqnService.EVENT_UPDATE, CqnService.EVENT_CREATE}, entity = Travel_.CDS_NAME)
public void calculateNewTotalPriceForActiveTravel(Travel travel) {
private void updateTravelPrice(String travelUUID, BigDecimal totalPrice) {
CqnUpdate update = Update.entity(TRAVEL).byId(travelUUID).data(Travel.TOTAL_PRICE, totalPrice);
draftService.patchDraft(update);
}

/*
* Elements annotated with @Core.computed are not transferred during
* DRAFT_SAVE. Normally, we'd re-compute the @Core.computed values after
* DRAFT_SAVE and store them to the active version. For the TravelStatus_code
* this is not possible as they originate as the result of a custom action
* and thus cannot be re-computed. We have to take them from the draft version and
* store them to the active version *before* the DRAFT_SAVE event.
*/
@On(event = { EVENT_DRAFT_CANCEL }, entity = Booking_.CDS_NAME)
public void updateTravelPriceOnCancelBooking(DraftCancelEventContext context) {
Booking booking = selectBookingPrice(entityKeys(context));
String travelUUID = booking.toTravelTravelUUID();
BigDecimal supplementPrice = calculateSupplementPrice(booking.bookingUUID());
BigDecimal totalPrice = selectTravelPrice(travelUUID).subtract(supplementPrice)
.subtract(nullToZero(booking.flightPrice()));

String travelUUID = travel.travelUUID();
if (StringUtils.isEmpty(travelUUID)) {
return;
}
travel.totalPrice(calculateTotalPriceForTravel(persistenceService, travelUUID, true));
updateTravelPrice(travelUUID, totalPrice);
}

private BigDecimal calculateSupplementPrice(String bookingUUID) {
Result result = draftService.run(Select.from(BOOKING_SUPPLEMENT).columns(s -> s.Price().sum().as("sum"))
.where(s -> s.to_Booking_BookingUUID().eq(bookingUUID).and(s.IsActiveEntity().eq(FALSE))));
Price price = result.single(Price.class);

Map<String, Object> data = new HashMap<>();
data.put(Travel.TOTAL_PRICE, travel.totalPrice());
data.put(Travel.TRAVEL_UUID, travelUUID);
persistenceService.run(Update.entity(TRAVEL).data(data));
return nullToZero(price.sum());
}

@After(event = { DraftService.EVENT_DRAFT_PATCH }, entity = Travel_.CDS_NAME)
public void recalculateTravelPriceIfTravelWasUpdated(final Travel travel) {
if (travel.travelUUID() != null && travel.bookingFee() != null) { // only for patched booking fee
String travelUUID = travel.travelUUID();
travel.totalPrice(calculateAndPatchNewTotalPriceForDraft(travelUUID));
@On(event = { EVENT_DRAFT_CANCEL }, entity = BookingSupplement_.CDS_NAME)
public void updateTravelPriceAfterDeleteBookingSupplement(DraftCancelEventContext context) {
BookingSupplement supplement = selectSupplementPrice(entityKeys(context));

if (supplement.price() != null) {
String travelUUID = supplement.toTravelTravelUUID();
updateTravelPrice(travelUUID, ZERO, supplement.price());
}
}

@After(event = { DraftService.EVENT_DRAFT_PATCH, DraftService.EVENT_DRAFT_NEW }, entity = Booking_.CDS_NAME)
public void recalculateTravelPriceIfFlightPriceWasUpdated(final Booking booking) {
draftService.run(Select.from(BOOKING).columns(bs -> bs.to_Travel().TravelUUID().as(Travel.TRAVEL_UUID))
.where(bs -> bs.BookingUUID().eq(booking.bookingUUID())
.and(bs.IsActiveEntity().eq(FALSE))))
.first()
.ifPresent(row -> calculateAndPatchNewTotalPriceForDraft((String) row.get(Travel.TRAVEL_UUID)));
private static Map<String, Object> entityKeys(DraftCancelEventContext context) {
return CqnAnalyzer.create(context.getModel()).analyze(context.getCqn()).targetKeys();
}

@After(event = { DraftService.EVENT_DRAFT_NEW, DraftService.EVENT_DRAFT_PATCH,
DraftService.EVENT_DRAFT_SAVE }, entity = BookingSupplement_.CDS_NAME)
public void recalculateTravelPriceIfPriceWasUpdated(final BookingSupplement bookingSupplement) {
draftService.run(Select.from(BOOKING_SUPPLEMENT)
.columns(bs -> bs.to_Booking().to_Travel().TravelUUID().as(Travel.TRAVEL_UUID))
.where(bs -> bs.BookSupplUUID().eq(bookingSupplement.bookSupplUUID())
.and(bs.IsActiveEntity().eq(FALSE))))
.first()
.ifPresent(row -> calculateAndPatchNewTotalPriceForDraft((String) row.get(Travel.TRAVEL_UUID)));
private BigDecimal selectTravelPrice(Object travelUUID) {
CqnSelect query = Select.from(TRAVEL).columns(t -> t.TotalPrice().as("sum")).matching(Map.of(
Travel.TRAVEL_UUID, travelUUID,
Travel.IS_ACTIVE_ENTITY, false));
BigDecimal price = draftService.run(query).single(Price.class).sum();

return nullToZero(price);
}

private BigDecimal calculateAndPatchNewTotalPriceForDraft(final String travelUUID) {
private BigDecimal selectTravelFee(Map<String, Object> travelKeys) {
CqnSelect query = Select.from(TRAVEL).matching(travelKeys).columns(b -> b.BookingFee());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe extract the query as a private static final and parameterize it for reuse

Travel travel = draftService.run(query).single(Travel.class);

BigDecimal totalPrice = calculateTotalPriceForTravel(draftService, travelUUID, false);
Map<String, Object> map = new HashMap<String, Object>();
map.put(Travel.TRAVEL_UUID, travelUUID);
map.put(Travel.TOTAL_PRICE, totalPrice);
CqnUpdate update = Update.entity(TRAVEL).data(map);
draftService.patchDraft(update);
return totalPrice;
return nullToZero(travel.bookingFee());
}

private Booking selectBookingPrice(Map<String, Object> bookingKeys) {
CqnSelect query = Select.from(BOOKING).matching(bookingKeys).columns(
b -> b.BookingUUID(), b -> b.to_Travel_TravelUUID(), b -> b.FlightPrice());
return draftService.run(query).single(Booking.class);
}

private BookingSupplement selectSupplementPrice(UnaryOperator<BookingSupplement> supplement) {
return selectSupplementPrice(supplement.apply(BookingSupplement.create()));
}

private BookingSupplement selectSupplementPrice(Map<String, Object> supplementKeys) {
CqnSelect query = Select.from(BOOKING_SUPPLEMENT).matching(supplementKeys)
.columns(s -> s.to_Booking().to_Travel_TravelUUID(), s -> s.Price());
return draftService.run(query).single(BookingSupplement.class);
}

private static BigDecimal nullToZero(BigDecimal d) {
return requireNonNullElse(d, ZERO);
}

@Before(event = { EVENT_CREATE, EVENT_UPDATE }, entity = { Booking_.CDS_NAME, BookingSupplement_.CDS_NAME })
public void disableUpdateAndCreateForBookingAndBookingSupplement() {
throw new ServiceException(ErrorStatuses.BAD_REQUEST, "error.booking.only_patch");
}

}