diff --git a/app/travels/field-control.cds b/app/travels/field-control.cds
index b50de98..cabc02f 100644
--- a/app/travels/field-control.cds
+++ b/app/travels/field-control.cds
@@ -29,7 +29,7 @@ annotate TravelService.Bookings with @UI.CreateHidden : (Travel.Status.code != #
annotate TravelService.Bookings with @UI.DeleteHidden : (Travel.Status.code != #Open);
annotate TravelService.Bookings {
- BookingDate @Core.Computed;
+ BookingDate @readonly;
Flight @readonly: (Travel.Status.code = #Accepted) @mandatory: (Travel.Status.code != #Accepted);
FlightPrice @readonly: (Travel.Status.code = #Accepted) @mandatory: (Travel.Status.code != #Accepted);
};
diff --git a/db/schema.cds b/db/schema.cds
index 53e934a..e22b261 100644
--- a/db/schema.cds
+++ b/db/schema.cds
@@ -13,7 +13,7 @@ entity Travels : managed {
BeginDate : Date default $now;
EndDate : Date default $now;
BookingFee : Price default 0;
- TotalPrice : Price @readonly;
+ TotalPrice : Price default 0 @readonly;
Currency : Currency default 'EUR';
Status : Association to TravelStatus default 'O';
Agency : Association to TravelAgencies;
diff --git a/srv/pom.xml b/srv/pom.xml
index ee923f0..af6b087 100644
--- a/srv/pom.xml
+++ b/srv/pom.xml
@@ -83,6 +83,12 @@
spring-boot-starter-test
test
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
diff --git a/srv/src/main/java/sap/capire/xtravels/handler/CreationHandler.java b/srv/src/main/java/sap/capire/xtravels/handler/CreationHandler.java
index bc97dce..01a50aa 100644
--- a/srv/src/main/java/sap/capire/xtravels/handler/CreationHandler.java
+++ b/srv/src/main/java/sap/capire/xtravels/handler/CreationHandler.java
@@ -13,6 +13,7 @@
import com.sap.cds.services.handler.EventHandler;
import com.sap.cds.services.handler.annotations.Before;
import com.sap.cds.services.handler.annotations.ServiceName;
+import java.time.LocalDate;
import org.springframework.stereotype.Component;
@Component
@@ -37,10 +38,18 @@ void calculateTravelId(final Travels travel) {
.columns(t -> t.ID().max().as("maxID")));
int maxId = (int) result.single().get("maxID");
travel.setId(++maxId);
+
+ if (travel.getBookings() != null) {
+ int nextPos = 1;
+ for (Bookings booking : travel.getBookings()) {
+ booking.setPos(nextPos++);
+ booking.setBookingDate(LocalDate.now()); // $now uses timestamp unexpectedly
+ }
+ }
}
// Fill in IDs as sequence numbers -> could be automated by auto-generation
- @Before(event = EVENT_DRAFT_NEW)
+ @Before(event = {EVENT_CREATE, EVENT_DRAFT_NEW})
void calculateBookingPos(Bookings_ ref, final Bookings booking) {
var result = service.run(Select.from(ref).columns(t -> t.Pos().max().as("maxPos")));
var maxPos = result.single().get("maxPos");
@@ -50,5 +59,6 @@ void calculateBookingPos(Bookings_ ref, final Bookings booking) {
int pos = (int) maxPos;
booking.setPos(++pos);
}
+ booking.setBookingDate(LocalDate.now());
}
}
diff --git a/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java
new file mode 100644
index 0000000..aca2429
--- /dev/null
+++ b/srv/src/test/java/sap/capire/xtravels/TravelServiceIntegrationTest.java
@@ -0,0 +1,330 @@
+package sap.capire.xtravels;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import cds.gen.travelservice.Bookings;
+import cds.gen.travelservice.Travels;
+import com.sap.cds.CdsData;
+import com.sap.cds.CdsJsonConverter;
+import com.sap.cds.CdsJsonConverter.UnknownPropertyHandling;
+import com.sap.cds.reflect.CdsModel;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.web.servlet.MockMvc;
+
+/** Integration tests for the CAP Travel Service OData endpoints */
+@SpringBootTest
+@AutoConfigureMockMvc
+class TravelServiceIntegrationTest {
+
+ private static final String ODATA_BASE_URL = "/odata/v4/travel";
+ private static final String TRAVELS_ENDPOINT = ODATA_BASE_URL + "/Travels";
+
+ @Autowired private MockMvc mockMvc;
+ @Autowired private CdsModel model;
+ private CdsJsonConverter converter;
+
+ @BeforeEach
+ void setup() {
+ converter =
+ CdsJsonConverter.builder(model)
+ .unknownPropertyHandling(UnknownPropertyHandling.IGNORE)
+ .build();
+ }
+
+ @Test
+ @WithMockUser("admin")
+ void shouldGetMetadataSuccessfully() throws Exception {
+ mockMvc
+ .perform(get(ODATA_BASE_URL + "/$metadata"))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith("application/xml"));
+ }
+
+ @Test
+ @WithMockUser("admin")
+ void shouldGetAllTravels() throws Exception {
+ mockMvc
+ .perform(get(TRAVELS_ENDPOINT))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith("application/json"))
+ .andExpect(jsonPath("$.@context", containsString("Travels")))
+ .andExpect(jsonPath("$.value").isArray());
+ }
+
+ @Test
+ @WithMockUser("admin")
+ void shouldCreateTravel() throws Exception {
+ Travels travel = createTravelData("shouldCreateTravel");
+
+ mockMvc
+ .perform(post(TRAVELS_ENDPOINT).contentType("application/json").content(travel.toJson()))
+ .andExpect(status().isCreated())
+ .andExpect(content().contentTypeCompatibleWith("application/json"))
+ .andExpect(jsonPath("$.Description").value(travel.getDescription()))
+ .andExpect(jsonPath("$.BeginDate").value(travel.getBeginDate().toString()))
+ .andExpect(jsonPath("$.EndDate").value(travel.getEndDate().toString()))
+ .andExpect(jsonPath("$.BookingFee").value(travel.getBookingFee().intValue()))
+ .andExpect(jsonPath("$.Currency_code").value(travel.getCurrencyCode()))
+ .andExpect(jsonPath("$.ID", notNullValue()));
+ }
+
+ @Test
+ @WithMockUser("admin")
+ void shouldCreateAndRetrieveTravelSuccessfully() throws Exception {
+ Travels travel = createTravelData("shouldCreateAndRetrieveTravelSuccessfully");
+ travel.setBookingFee(BigDecimal.valueOf(200.0));
+ travel.setCurrencyCode("USD");
+
+ // Create travel
+ String response =
+ mockMvc
+ .perform(
+ post(TRAVELS_ENDPOINT).contentType("application/json").content(travel.toJson()))
+ .andExpect(status().isCreated())
+ .andReturn()
+ .getResponse()
+ .getContentAsString();
+
+ // Verify the created travel can be retrieved
+ Travels createdTravel = converter.fromJsonObject(response, Travels.class);
+ assertNotNull(createdTravel.getId());
+
+ mockMvc
+ .perform(get(TRAVELS_ENDPOINT + "(ID=" + createdTravel.getId() + ",IsActiveEntity=true)"))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith("application/json"))
+ .andExpect(jsonPath("$.BookingFee").value(200.0))
+ .andExpect(jsonPath("$.Currency_code").value("USD"));
+ }
+
+ @Test
+ @WithMockUser("admin")
+ void shouldCreateAndRetrieveTravelWithBookingsSuccessfully() throws Exception {
+ Travels travel = createTravelData("shouldCreateAndRetrieveTravelWithBookingsSuccessfully");
+ Bookings booking = Bookings.create();
+ booking.setFlightId("GA0322");
+ booking.setFlightDate(LocalDate.of(2024, 6, 2));
+ booking.setFlightPrice(BigDecimal.valueOf(1103));
+ Bookings.Supplements supplement = Bookings.Supplements.create();
+ supplement.setBookedId("bv-0001");
+ supplement.setPrice(new BigDecimal("2.30"));
+ booking.setSupplements(List.of(supplement));
+ travel.setBookings(List.of(booking));
+
+ // Create travel
+ String response =
+ mockMvc
+ .perform(
+ post(TRAVELS_ENDPOINT).contentType("application/json").content(travel.toJson()))
+ .andExpect(status().isCreated())
+ .andReturn()
+ .getResponse()
+ .getContentAsString();
+
+ // Verify the created travel can be retrieved
+ Travels createdTravel = converter.fromJsonObject(response, Travels.class);
+ assertNotNull(createdTravel.getId());
+
+ // Verify @federated data can be read
+ mockMvc
+ .perform(
+ get(
+ TRAVELS_ENDPOINT
+ + "(ID="
+ + createdTravel.getId()
+ + ",IsActiveEntity=true)?$expand=Bookings($expand=Flight,Supplements($expand=booked))"))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith("application/json"))
+ .andExpect(jsonPath("$.Bookings[0].Flight.origin").value("Miami International Airport"))
+ .andExpect(jsonPath("$.Bookings[0].Supplements[0].booked.descr").value("Hot Chocolate"));
+ }
+
+ @Test
+ @WithMockUser("admin")
+ void shouldGetReadOnlyEntitiesSuccessfully() throws Exception {
+ // Test Flights entity
+ mockMvc
+ .perform(get(ODATA_BASE_URL + "/Flights"))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith("application/json"));
+
+ // Test Supplements entity
+ mockMvc
+ .perform(get(ODATA_BASE_URL + "/Supplements"))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith("application/json"));
+
+ // Test Currencies entity
+ mockMvc
+ .perform(get(ODATA_BASE_URL + "/Currencies"))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith("application/json"));
+ }
+
+ @Test
+ @WithMockUser("admin")
+ void shouldReturn400ForInvalidDiscountPercentage() throws Exception {
+ // First create a travel
+ Travels travelData = createTravelData("shouldReturn400ForInvalidDiscountPercentage");
+ String response =
+ mockMvc
+ .perform(
+ post(TRAVELS_ENDPOINT).contentType("application/json").content(travelData.toJson()))
+ .andExpect(status().isCreated())
+ .andReturn()
+ .getResponse()
+ .getContentAsString();
+
+ Travels createdTravel = converter.fromJsonObject(response, Travels.class);
+
+ // Try to execute deductDiscount action with invalid percentage (>100)
+ CdsData actionParams = CdsData.create();
+ actionParams.put("percent", 150); // Invalid percentage
+
+ mockMvc
+ .perform(
+ post(TRAVELS_ENDPOINT
+ + "(ID="
+ + createdTravel.getId()
+ + ",IsActiveEntity=true)/TravelService.deductDiscount")
+ .contentType("application/json")
+ .content(actionParams.toJson()))
+ .andExpect(status().isBadRequest());
+ }
+
+ @Test
+ @WithMockUser("admin")
+ void shouldExecuteAcceptTravelAction() throws Exception {
+ // First create a travel
+ Travels travelData = createTravelData("shouldExecuteAcceptTravelAction");
+ String response =
+ mockMvc
+ .perform(
+ post(TRAVELS_ENDPOINT).contentType("application/json").content(travelData.toJson()))
+ .andExpect(status().isCreated())
+ .andReturn()
+ .getResponse()
+ .getContentAsString();
+
+ Travels createdTravel = converter.fromJsonObject(response, Travels.class);
+
+ // Execute acceptTravel action
+ mockMvc
+ .perform(
+ post(TRAVELS_ENDPOINT
+ + "(ID="
+ + createdTravel.getId()
+ + ",IsActiveEntity=true)/TravelService.acceptTravel")
+ .contentType("application/json")
+ .content("{}"))
+ .andExpect(status().is2xxSuccessful());
+
+ // Check if travel status is accepted
+ mockMvc
+ .perform(get(TRAVELS_ENDPOINT + "(ID=" + createdTravel.getId() + ",IsActiveEntity=true)"))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith("application/json"))
+ .andExpect(jsonPath("$.Status_code").value("A"));
+ }
+
+ @Test
+ @WithMockUser("admin")
+ void shouldExecuteRejectTravelAction() throws Exception {
+ // First create a travel
+ Travels travelData = createTravelData("shouldExecuteRejectTravelAction");
+ String response =
+ mockMvc
+ .perform(
+ post(TRAVELS_ENDPOINT).contentType("application/json").content(travelData.toJson()))
+ .andExpect(status().isCreated())
+ .andReturn()
+ .getResponse()
+ .getContentAsString();
+
+ Travels createdTravel = converter.fromJsonObject(response, Travels.class);
+
+ // Execute rejectTravel action
+ mockMvc
+ .perform(
+ post(TRAVELS_ENDPOINT
+ + "(ID="
+ + createdTravel.getId()
+ + ",IsActiveEntity=true)/TravelService.rejectTravel")
+ .contentType("application/json")
+ .content("{}"))
+ .andExpect(status().is2xxSuccessful());
+
+ // Check if travel status is rejected
+ mockMvc
+ .perform(get(TRAVELS_ENDPOINT + "(ID=" + createdTravel.getId() + ",IsActiveEntity=true)"))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith("application/json"))
+ .andExpect(jsonPath("$.Status_code").value("X"));
+ }
+
+ @Test
+ @WithMockUser("admin")
+ void shouldExecuteDeductDiscountAction() throws Exception {
+ // First create a travel
+ Travels travelData = createTravelData("shouldExecuteDeductDiscountAction");
+
+ String response =
+ mockMvc
+ .perform(
+ post(TRAVELS_ENDPOINT).contentType("application/json").content(travelData.toJson()))
+ .andExpect(status().isCreated())
+ .andReturn()
+ .getResponse()
+ .getContentAsString();
+
+ Travels createdTravel = converter.fromJsonObject(response, Travels.class);
+
+ // Execute deductDiscount action with 10% discount
+ CdsData actionParams = CdsData.create();
+ actionParams.put("percent", 10);
+
+ mockMvc
+ .perform(
+ post(TRAVELS_ENDPOINT
+ + "(ID="
+ + createdTravel.getId()
+ + ",IsActiveEntity=true)/TravelService.deductDiscount")
+ .contentType("application/json")
+ .content(actionParams.toJson()))
+ .andExpect(status().isOk());
+
+ mockMvc
+ .perform(get(TRAVELS_ENDPOINT + "(ID=" + createdTravel.getId() + ",IsActiveEntity=true)"))
+ .andExpect(status().isOk())
+ .andExpect(content().contentTypeCompatibleWith("application/json"))
+ .andExpect(jsonPath("$.BookingFee").value(90));
+ }
+
+ private Travels createTravelData(String testName) {
+ Travels travel = Travels.create();
+ travel.setIsActiveEntity(true);
+ travel.setDescription(testName + " - Test Travel");
+ travel.setBeginDate(LocalDate.of(2024, 6, 1));
+ travel.setEndDate(LocalDate.of(2024, 6, 14));
+ travel.setBookingFee(BigDecimal.valueOf(100));
+ travel.setCurrencyCode("EUR");
+ travel.setAgencyId("070001");
+ travel.setCustomerId("000001");
+ return travel;
+ }
+}