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 2 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,31 +3,37 @@
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 org.springframework.stereotype.Component;

import com.sap.cds.Row;
import com.google.common.base.Strings;
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.travelservice.Booking;
import cds.gen.travelservice.BookingSupplement;
Expand All @@ -41,6 +47,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 +57,159 @@ 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 (!Strings.isNullOrEmpty(travelUUID)) {
BigDecimal totalPrice = calculateTravelPrice(travelUUID);
travel.totalPrice(totalPrice);

persistenceService.run(Update.entity(TRAVEL).data(Map.of(
Travel.TRAVEL_UUID, travelUUID,
Travel.TOTAL_PRICE, totalPrice)));
}
}

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 calculateTravelPrice(String travelUUID) {
BigDecimal bookingFee = run(Select.from(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

.where(t -> t.TravelUUID().eq(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 BigDecimal run(CqnSelect query) {
BigDecimal sum = persistenceService.run(query).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 static interface Price {
BigDecimal sum();
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think this interface buys much


@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) {
Map<String, Object> travelKeys = CqnAnalyzer.create(context.getModel()).analyze(update).targetKeys();
Copy link
Collaborator

Choose a reason for hiding this comment

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

CqnAnalyzer.create(context.getModel()) -> we should offer this directly on the "event context"

Copy link
Collaborator

Choose a reason for hiding this comment

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

Access travelKeys as Travel ?

e.g.

Travel travelKeys = CqnAnalyzer.create(context.getModel()).analyze(update).targetKeys(Travel.class);
BigDecimal oldFee = selectTravelFee(travelKeys);
BigDecimal travelPrice = selectTravelPrice(travelKeys.getTravelUuid()).add(newFee).subtract(oldFee);

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 = selectBooking(Map.of(
Booking.BOOKING_UUID, bookingPatch.bookingUUID(),
Booking.IS_ACTIVE_ENTITY, 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 = selectSupplement(Map.of(BookingSupplement.BOOK_SUPPL_UUID,
supplementPatch.bookSupplUUID(), BookingSupplement.IS_ACTIVE_ENTITY, 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) {
draftService.patchDraft(Update.entity(TRAVEL).byId(travelUUID).data(Travel.TOTAL_PRICE, totalPrice));
}

/*
* 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 = selectBooking(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);
}

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));
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))));

return nullToZero(result.single(Price.class).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 = selectSupplement(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();
}

private BigDecimal selectTravelPrice(Object travelUUID) {
CqnSelect query = Select.from(TRAVEL)
.matching(Map.of(Travel.TRAVEL_UUID, travelUUID, Travel.IS_ACTIVE_ENTITY, false))
.columns(t -> t.TotalPrice().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.

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

BigDecimal price = draftService.run(query).single(Price.class).sum();

return nullToZero(price);
}

@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 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);

return nullToZero(travel.bookingFee());
}

private BigDecimal calculateAndPatchNewTotalPriceForDraft(final String travelUUID) {
private Booking selectBooking(Map<String, Object> bookingKeys) {
CqnSelect query = Select.from(BOOKING).matching(bookingKeys).columns(b -> b.BookingUUID(),
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

b -> b.to_Travel_TravelUUID(), b -> b.FlightPrice());
return draftService.run(query).single(Booking.class);
}

private BookingSupplement selectSupplement(Map<String, Object> supplementKeys) {
return draftService.run(Select.from(BOOKING_SUPPLEMENT).matching(supplementKeys)
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

.columns(s -> s.to_Booking().to_Travel_TravelUUID(), s -> s.Price())).single(BookingSupplement.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;
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");
}

}