Skip to content
Merged
Show file tree
Hide file tree
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
5 changes: 4 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-webmvc")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-amqp")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")

// Database
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
Expand All @@ -45,8 +47,9 @@ dependencies {
annotationProcessor("org.mapstruct:mapstruct-processor:1.6.3")
annotationProcessor("org.projectlombok:lombok-mapstruct-binding:0.2.0")

// gRPC Server
// gRPC Server and Client
implementation("net.devh:grpc-server-spring-boot-starter:3.1.0.RELEASE")
implementation("net.devh:grpc-client-spring-boot-starter:3.1.0.RELEASE")
implementation("io.grpc:grpc-protobuf:$grpcVersion")
implementation("io.grpc:grpc-stub:$grpcVersion")
implementation("io.grpc:grpc-netty-shaded:$grpcVersion")
Expand Down
14 changes: 13 additions & 1 deletion environment/.local.env
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,16 @@ POSTGRES_HOST=devoops-postgres
POSTGRES_PORT=5432
DB_USERNAME=reservation-service
DB_PASSWORD=reservation-service-pass
GRPC_PORT=9090

# gRPC Server
GRPC_PORT=9090

# gRPC Client
ACCOMMODATION_GRPC_HOST=devoops-accommodation-service
ACCOMMODATION_GRPC_PORT=9090

# RabbitMQ
RABBITMQ_HOST=devoops-rabbitmq
RABBITMQ_PORT=5672
RABBITMQ_USERNAME=devoops
RABBITMQ_PASSWORD=devoops123
29 changes: 29 additions & 0 deletions src/main/java/com/devoops/reservation/config/RabbitMQConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.devoops.reservation.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {

@Value("${rabbitmq.exchange.notification}")
private String notificationExchange;

@Bean
public TopicExchange notificationExchange() {
return new TopicExchange(notificationExchange);
}

@Bean
public MessageConverter jsonMessageConverter() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
return new Jackson2JsonMessageConverter(objectMapper);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,13 @@ public ResponseEntity<Void> delete(
reservationService.deleteRequest(id, userContext);
return ResponseEntity.noContent().build();
}

@PostMapping("/{id}/cancel")
@RequireRole("GUEST")
public ResponseEntity<Void> cancel(
@PathVariable UUID id,
UserContext userContext) {
reservationService.cancelReservation(id, userContext);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.devoops.reservation.dto.message;

import java.time.LocalDate;
import java.util.UUID;

public record ReservationCancelledMessage(
UUID userId,
String userEmail,
String guestName,
String accommodationName,
LocalDate checkIn,
LocalDate checkOut,
String reason
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.devoops.reservation.dto.message;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.UUID;

public record ReservationCreatedMessage(
UUID userId,
String userEmail,
String guestName,
String accommodationName,
LocalDate checkIn,
LocalDate checkOut,
BigDecimal totalPrice
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.devoops.reservation.exception;

public class AccommodationNotFoundException extends RuntimeException {

public AccommodationNotFoundException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ public ProblemDetail handleNotFound(ReservationNotFoundException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
}

@ExceptionHandler(AccommodationNotFoundException.class)
public ProblemDetail handleAccommodationNotFound(AccommodationNotFoundException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
}

@ExceptionHandler(UnauthorizedException.class)
public ProblemDetail handleUnauthorized(UnauthorizedException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, ex.getMessage());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.devoops.reservation.grpc;

import com.devoops.reservation.grpc.proto.accommodation.AccommodationInternalServiceGrpc;
import com.devoops.reservation.grpc.proto.accommodation.ReservationValidationRequest;
import com.devoops.reservation.grpc.proto.accommodation.ReservationValidationResponse;
import lombok.extern.slf4j.Slf4j;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.UUID;

@Component
@Slf4j
public class AccommodationGrpcClient {

@GrpcClient("accommodation-service")
private AccommodationInternalServiceGrpc.AccommodationInternalServiceBlockingStub accommodationStub;

public AccommodationValidationResult validateAndCalculatePrice(
UUID accommodationId,
LocalDate startDate,
LocalDate endDate,
int guestCount) {

log.debug("Calling accommodation service for validation: accommodationId={}, dates={} to {}, guests={}",
accommodationId, startDate, endDate, guestCount);

ReservationValidationRequest request = ReservationValidationRequest.newBuilder()
.setAccommodationId(accommodationId.toString())
.setStartDate(startDate.toString())
.setEndDate(endDate.toString())
.setGuestCount(guestCount)
.build();

ReservationValidationResponse response = accommodationStub.validateAndCalculatePrice(request);

log.debug("Received validation response: valid={}, errorCode={}", response.getValid(), response.getErrorCode());

if (!response.getValid()) {
return new AccommodationValidationResult(
false,
response.getErrorCode(),
response.getErrorMessage(),
null,
null,
null,
null,
null
);
}

return new AccommodationValidationResult(
true,
null,
null,
UUID.fromString(response.getHostId()),
new BigDecimal(response.getTotalPrice()),
response.getPricingMode(),
response.getApprovalMode(),
response.getAccommodationName()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.devoops.reservation.grpc;

import java.math.BigDecimal;
import java.util.UUID;

public record AccommodationValidationResult(
boolean valid,
String errorCode,
String errorMessage,
UUID hostId,
BigDecimal totalPrice,
String pricingMode,
String approvalMode,
String accommodationName
) {
public boolean isAutoApproval() {
return "AUTOMATIC".equals(approvalMode);
}
}
50 changes: 50 additions & 0 deletions src/main/java/com/devoops/reservation/grpc/UserGrpcClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.devoops.reservation.grpc;

import com.devoops.reservation.grpc.proto.user.GetUserSummaryRequest;
import com.devoops.reservation.grpc.proto.user.GetUserSummaryResponse;
import com.devoops.reservation.grpc.proto.user.UserInternalServiceGrpc;
import lombok.extern.slf4j.Slf4j;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.springframework.stereotype.Component;

import java.util.UUID;

@Component
@Slf4j
public class UserGrpcClient {

@GrpcClient("user-service")
private UserInternalServiceGrpc.UserInternalServiceBlockingStub userStub;

public UserSummaryResult getUserSummary(UUID userId) {
log.debug("Calling user service for user summary: userId={}", userId);

GetUserSummaryRequest request = GetUserSummaryRequest.newBuilder()
.setUserId(userId.toString())
.build();

GetUserSummaryResponse response = userStub.getUserSummary(request);

log.debug("Received user summary response: found={}", response.getFound());

if (!response.getFound()) {
return new UserSummaryResult(
false,
null,
null,
null,
null,
null
);
}

return new UserSummaryResult(
true,
UUID.fromString(response.getUserId()),
response.getEmail(),
response.getFirstName(),
response.getLastName(),
response.getRole()
);
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/devoops/reservation/grpc/UserSummaryResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.devoops.reservation.grpc;

import java.util.UUID;

public record UserSummaryResult(
boolean found,
UUID userId,
String email,
String firstName,
String lastName,
String role
) {
public String getFullName() {
return firstName + " " + lastName;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.devoops.reservation.service;

import com.devoops.reservation.dto.message.ReservationCancelledMessage;
import com.devoops.reservation.dto.message.ReservationCreatedMessage;
import com.devoops.reservation.entity.Reservation;
import com.devoops.reservation.grpc.UserGrpcClient;
import com.devoops.reservation.grpc.UserSummaryResult;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class ReservationEventPublisherService {

private final RabbitTemplate rabbitTemplate;
private final UserGrpcClient userGrpcClient;

@Value("${rabbitmq.exchange.notification}")
private String notificationExchange;

@Value("${rabbitmq.routing-key.reservation-created}")
private String reservationCreatedRoutingKey;

@Value("${rabbitmq.routing-key.reservation-cancelled}")
private String reservationCancelledRoutingKey;

public void publishReservationCreated(Reservation reservation, String accommodationName) {
UserSummaryResult hostSummary = userGrpcClient.getUserSummary(reservation.getHostId());
UserSummaryResult guestSummary = userGrpcClient.getUserSummary(reservation.getGuestId());

if (!hostSummary.found()) {
log.warn("Host not found for reservation {}, skipping notification", reservation.getId());
return;
}

if (!guestSummary.found()) {
log.warn("Guest not found for reservation {}, skipping notification", reservation.getId());
return;
}

ReservationCreatedMessage message = new ReservationCreatedMessage(
reservation.getHostId(),
hostSummary.email(),
guestSummary.getFullName(),
accommodationName,
reservation.getStartDate(),
reservation.getEndDate(),
reservation.getTotalPrice()
);

log.info("Publishing reservation created event: reservationId={}, hostEmail={}, guestName={}",
reservation.getId(), hostSummary.email(), guestSummary.getFullName());

rabbitTemplate.convertAndSend(notificationExchange, reservationCreatedRoutingKey, message);
}

public void publishReservationCancelled(Reservation reservation, String accommodationName) {
UserSummaryResult hostSummary = userGrpcClient.getUserSummary(reservation.getHostId());
UserSummaryResult guestSummary = userGrpcClient.getUserSummary(reservation.getGuestId());

if (!hostSummary.found()) {
log.warn("Host not found for reservation {}, skipping notification", reservation.getId());
return;
}

if (!guestSummary.found()) {
log.warn("Guest not found for reservation {}, skipping notification", reservation.getId());
return;
}

ReservationCancelledMessage message = new ReservationCancelledMessage(
reservation.getHostId(),
hostSummary.email(),
guestSummary.getFullName(),
accommodationName,
reservation.getStartDate(),
reservation.getEndDate(),
"Guest cancelled the reservation"
);

log.info("Publishing reservation cancelled event: reservationId={}, hostEmail={}, guestName={}",
reservation.getId(), hostSummary.email(), guestSummary.getFullName());

rabbitTemplate.convertAndSend(notificationExchange, reservationCancelledRoutingKey, message);
}
}
Loading