diff --git a/srv/src/main/java/com/sap/cap/sflight/processor/RecalculatePriceHandler.java b/srv/src/main/java/com/sap/cap/sflight/processor/RecalculatePriceHandler.java index 2ef98615..22c9035b 100644 --- a/srv/src/main/java/com/sap/cap/sflight/processor/RecalculatePriceHandler.java +++ b/srv/src/main/java/com/sap/cap/sflight/processor/RecalculatePriceHandler.java @@ -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_; @@ -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; @@ -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")) + .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 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); + } + + 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); + 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 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 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 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 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 travelKeys) { + CqnSelect query = Select.from(TRAVEL).matching(travelKeys).columns(b -> b.BookingFee()); + Travel travel = draftService.run(query).single(Travel.class); - BigDecimal totalPrice = calculateTotalPriceForTravel(draftService, travelUUID, false); - Map map = new HashMap(); - 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 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 supplement) { + return selectSupplementPrice(supplement.apply(BookingSupplement.create())); + } + + private BookingSupplement selectSupplementPrice(Map 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"); } }