From 3505fbdc3d2f253f8b657a4f0350926a86655fdf Mon Sep 17 00:00:00 2001 From: "roman.debonis" Date: Tue, 28 Apr 2026 22:19:51 +0200 Subject: [PATCH 1/2] basic checkout implementation entities. --- .../getourguide/interview/entity/Order.java | 40 +++++++++++++ .../interview/repository/OrderRepository.java | 9 +++ .../interview/service/CheckoutService.java | 58 +++++++++++++++++++ .../migration/V1.0.3__create_orders_table.sql | 12 ++++ 4 files changed, 119 insertions(+) create mode 100644 src/main/java/com/getourguide/interview/entity/Order.java create mode 100644 src/main/java/com/getourguide/interview/repository/OrderRepository.java create mode 100644 src/main/java/com/getourguide/interview/service/CheckoutService.java create mode 100644 src/main/resources/db/migration/V1.0.3__create_orders_table.sql diff --git a/src/main/java/com/getourguide/interview/entity/Order.java b/src/main/java/com/getourguide/interview/entity/Order.java new file mode 100644 index 0000000..a8847f0 --- /dev/null +++ b/src/main/java/com/getourguide/interview/entity/Order.java @@ -0,0 +1,40 @@ +package com.getourguide.interview.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.hibernate.annotations.NotFound; +import org.hibernate.annotations.NotFoundAction; + +@Getter +@Setter +@ToString +@AllArgsConstructor +@Entity +@Table(schema = "getyourguide", name = "orders") +@NoArgsConstructor +@EqualsAndHashCode +public class Order { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "activity_id") + @NotFound(action = NotFoundAction.IGNORE) + @ToString.Exclude + private Activity activity; + private String bookingReference; + private double price; + private String status; +} diff --git a/src/main/java/com/getourguide/interview/repository/OrderRepository.java b/src/main/java/com/getourguide/interview/repository/OrderRepository.java new file mode 100644 index 0000000..27a239a --- /dev/null +++ b/src/main/java/com/getourguide/interview/repository/OrderRepository.java @@ -0,0 +1,9 @@ +package com.getourguide.interview.repository; + +import com.getourguide.interview.entity.Order; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface OrderRepository extends JpaRepository { +} diff --git a/src/main/java/com/getourguide/interview/service/CheckoutService.java b/src/main/java/com/getourguide/interview/service/CheckoutService.java new file mode 100644 index 0000000..40082e7 --- /dev/null +++ b/src/main/java/com/getourguide/interview/service/CheckoutService.java @@ -0,0 +1,58 @@ +package com.getourguide.interview.service; + +import com.getourguide.interview.entity.Order; +import com.getourguide.interview.repository.OrderRepository; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@AllArgsConstructor +public class CheckoutService { + + private final OrderRepository orderRepository; + + @Transactional + public void checkout(Long orderId) throws Exception { + Order order = orderRepository.findById(orderId).orElseThrow(); + order.setStatus("RESERVED"); + orderRepository.save(order); + + chargeCustomer(order); + sendConfirmationEmail(order); + } + + private void chargeCustomer(Order order) throws Exception { + + String body = """ + {"orderId": %d, "amount": %s} + """.formatted(order.getId(), order.getPrice()); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:8081/payments/charge")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + + HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); + } + + private void sendConfirmationEmail(Order order) throws Exception { + + String body = """ + {"orderId": %d, "bookingReference": "%s"} + """.formatted(order.getId(), order.getBookingReference()); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:8082/emails/confirmation")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + + HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); + } +} diff --git a/src/main/resources/db/migration/V1.0.3__create_orders_table.sql b/src/main/resources/db/migration/V1.0.3__create_orders_table.sql new file mode 100644 index 0000000..4713026 --- /dev/null +++ b/src/main/resources/db/migration/V1.0.3__create_orders_table.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS getyourguide.orders ( + id LONG PRIMARY KEY, + activity_id LONG, + booking_reference VARCHAR(64), + price FLOAT, + status VARCHAR(32) +); + +INSERT INTO getyourguide.orders (id, activity_id, booking_reference, price, status) VALUES +(1, 25651, 'BR-0001', 14, 'PENDING'), +(2, 6960, 'BR-0002', 21, 'PENDING'), +(3, 26823, 'BR-0003', 41, 'PENDING'); From 5a7a1d6e3d1684730fbcf18ceec5bbb91bb1924b Mon Sep 17 00:00:00 2001 From: "roman.debonis" Date: Wed, 29 Apr 2026 18:38:47 +0200 Subject: [PATCH 2/2] basic checkout implementation entities. --- build.gradle | 1 + .../interview/GetYourGuideApplication.java | 2 + .../interview/service/CheckoutService.java | 11 +++-- .../CheckoutServiceIntegrationTest.java | 47 +++++++++++++++++++ 4 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 src/test/java/com/getourguide/interview/service/CheckoutServiceIntegrationTest.java diff --git a/build.gradle b/build.gradle index 64e130a..57ddf3e 100644 --- a/build.gradle +++ b/build.gradle @@ -35,6 +35,7 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' + testImplementation 'org.wiremock:wiremock-standalone:3.9.1' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/src/main/java/com/getourguide/interview/GetYourGuideApplication.java b/src/main/java/com/getourguide/interview/GetYourGuideApplication.java index c80cf8c..6131042 100644 --- a/src/main/java/com/getourguide/interview/GetYourGuideApplication.java +++ b/src/main/java/com/getourguide/interview/GetYourGuideApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableScheduling public class GetYourGuideApplication { public static void main(String[] args) { diff --git a/src/main/java/com/getourguide/interview/service/CheckoutService.java b/src/main/java/com/getourguide/interview/service/CheckoutService.java index 40082e7..6901859 100644 --- a/src/main/java/com/getourguide/interview/service/CheckoutService.java +++ b/src/main/java/com/getourguide/interview/service/CheckoutService.java @@ -7,6 +7,7 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import lombok.AllArgsConstructor; +import lombok.SneakyThrows; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,7 +18,7 @@ public class CheckoutService { private final OrderRepository orderRepository; @Transactional - public void checkout(Long orderId) throws Exception { + public void checkout(Long orderId) { Order order = orderRepository.findById(orderId).orElseThrow(); order.setStatus("RESERVED"); orderRepository.save(order); @@ -26,7 +27,8 @@ public void checkout(Long orderId) throws Exception { sendConfirmationEmail(order); } - private void chargeCustomer(Order order) throws Exception { + @SneakyThrows + void chargeCustomer(Order order) { String body = """ {"orderId": %d, "amount": %s} @@ -41,14 +43,15 @@ private void chargeCustomer(Order order) throws Exception { HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); } - private void sendConfirmationEmail(Order order) throws Exception { + @SneakyThrows + void sendConfirmationEmail(Order order) { String body = """ {"orderId": %d, "bookingReference": "%s"} """.formatted(order.getId(), order.getBookingReference()); HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:8082/emails/confirmation")) + .uri(URI.create("http://localhost:8081/emails/confirmation")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); diff --git a/src/test/java/com/getourguide/interview/service/CheckoutServiceIntegrationTest.java b/src/test/java/com/getourguide/interview/service/CheckoutServiceIntegrationTest.java new file mode 100644 index 0000000..b63dbf8 --- /dev/null +++ b/src/test/java/com/getourguide/interview/service/CheckoutServiceIntegrationTest.java @@ -0,0 +1,47 @@ +package com.getourguide.interview.service; + +import com.getourguide.interview.entity.Order; +import com.getourguide.interview.repository.OrderRepository; +import com.github.tomakehurst.wiremock.WireMockServer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +class CheckoutServiceIntegrationTest { + + private static WireMockServer wireMock; + + @Autowired private CheckoutService checkoutService; + @Autowired private OrderRepository orderRepository; + + @BeforeAll + static void startWireMock() { + wireMock = new WireMockServer(wireMockConfig().port(8081)); + wireMock.start(); + wireMock.stubFor(post(urlEqualTo("/payments/charge")).willReturn(aResponse().withStatus(200))); + wireMock.stubFor(post(urlEqualTo("/emails/confirmation")).willReturn(aResponse().withStatus(200))); + } + + @AfterAll + static void stopWireMock() { + wireMock.stop(); + } + + @Test + void checkoutReservesOrder() { + Order order = orderRepository.findAll().getFirst(); + + checkoutService.checkout(order.getId()); + + assertEquals("RESERVED", orderRepository.findById(order.getId()).orElseThrow().getStatus()); + } +}