diff --git a/build.gradle.kts b/build.gradle.kts index d226bb3..e01db7b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,8 +52,9 @@ dependencies { // MinIO S3-compatible object storage implementation("io.minio:minio:8.5.7") - // gRPC Client + // gRPC Client and Server implementation("net.devh:grpc-client-spring-boot-starter:3.1.0.RELEASE") + implementation("net.devh:grpc-server-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") diff --git a/environment/.local.env b/environment/.local.env index 375ba1e..d6156a8 100644 --- a/environment/.local.env +++ b/environment/.local.env @@ -13,6 +13,9 @@ MINIO_ACCESS_KEY=devoops MINIO_SECRET_KEY=devoops123 MINIO_BUCKET=accommodation-photos -# gRPC +# gRPC Server +GRPC_PORT=9090 + +# gRPC Client RESERVATION_GRPC_HOST=devoops-reservation-service RESERVATION_GRPC_PORT=9090 diff --git a/src/main/java/com/devoops/accommodation/grpc/AccommodationGrpcService.java b/src/main/java/com/devoops/accommodation/grpc/AccommodationGrpcService.java new file mode 100644 index 0000000..2f13bc0 --- /dev/null +++ b/src/main/java/com/devoops/accommodation/grpc/AccommodationGrpcService.java @@ -0,0 +1,129 @@ +package com.devoops.accommodation.grpc; + +import com.devoops.accommodation.entity.Accommodation; +import com.devoops.accommodation.entity.AvailabilityPeriod; +import com.devoops.accommodation.entity.PricingMode; +import com.devoops.accommodation.grpc.proto.AccommodationInternalServiceGrpc; +import com.devoops.accommodation.grpc.proto.ReservationValidationRequest; +import com.devoops.accommodation.grpc.proto.ReservationValidationResponse; +import com.devoops.accommodation.repository.AccommodationRepository; +import com.devoops.accommodation.repository.AvailabilityPeriodRepository; +import io.grpc.stub.StreamObserver; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.server.service.GrpcService; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; +import java.util.Optional; +import java.util.UUID; + +@GrpcService +@RequiredArgsConstructor +@Slf4j +public class AccommodationGrpcService extends AccommodationInternalServiceGrpc.AccommodationInternalServiceImplBase { + + private final AccommodationRepository accommodationRepository; + private final AvailabilityPeriodRepository availabilityPeriodRepository; + + @Override + public void validateAndCalculatePrice( + ReservationValidationRequest request, + StreamObserver responseObserver) { + + log.debug("Received validation request for accommodation: {}", request.getAccommodationId()); + + ReservationValidationResponse response = processValidation(request); + + responseObserver.onNext(response); + responseObserver.onCompleted(); + } + + private ReservationValidationResponse processValidation(ReservationValidationRequest request) { + UUID accommodationId; + LocalDate startDate; + LocalDate endDate; + + try { + accommodationId = UUID.fromString(request.getAccommodationId()); + } catch (IllegalArgumentException e) { + return buildErrorResponse("ACCOMMODATION_NOT_FOUND", "Invalid accommodation ID format"); + } + + try { + startDate = LocalDate.parse(request.getStartDate()); + endDate = LocalDate.parse(request.getEndDate()); + } catch (DateTimeParseException e) { + return buildErrorResponse("INVALID_DATES", "Invalid date format. Use ISO format (yyyy-MM-dd)"); + } + + Optional accommodationOpt = accommodationRepository.findById(accommodationId); + if (accommodationOpt.isEmpty()) { + log.debug("Accommodation not found: {}", accommodationId); + return buildErrorResponse("ACCOMMODATION_NOT_FOUND", "Accommodation not found"); + } + + Accommodation accommodation = accommodationOpt.get(); + int guestCount = request.getGuestCount(); + + if (guestCount < accommodation.getMinGuests() || guestCount > accommodation.getMaxGuests()) { + log.debug("Invalid guest count {} for accommodation {} (min: {}, max: {})", + guestCount, accommodationId, accommodation.getMinGuests(), accommodation.getMaxGuests()); + return buildErrorResponse("GUEST_COUNT_INVALID", + String.format("Guest count must be between %d and %d", + accommodation.getMinGuests(), accommodation.getMaxGuests())); + } + + Optional periodOpt = availabilityPeriodRepository + .findCoveringPeriod(accommodationId, startDate, endDate); + + if (periodOpt.isEmpty()) { + log.debug("No availability period covers dates {} to {} for accommodation {}", + startDate, endDate, accommodationId); + return buildErrorResponse("DATES_NOT_AVAILABLE", + "The selected dates are not within an available period"); + } + + AvailabilityPeriod period = periodOpt.get(); + BigDecimal totalPrice = calculateTotalPrice(period, accommodation.getPricingMode(), startDate, endDate, guestCount); + + log.info("Validation successful for accommodation {}: totalPrice={}, approvalMode={}", + accommodationId, totalPrice, accommodation.getApprovalMode()); + + return ReservationValidationResponse.newBuilder() + .setValid(true) + .setHostId(accommodation.getHostId().toString()) + .setTotalPrice(totalPrice.toPlainString()) + .setPricingMode(accommodation.getPricingMode().name()) + .setApprovalMode(accommodation.getApprovalMode().name()) + .setAccommodationName(accommodation.getName()) + .build(); + } + + private BigDecimal calculateTotalPrice( + AvailabilityPeriod period, + PricingMode pricingMode, + LocalDate startDate, + LocalDate endDate, + int guestCount) { + + long nights = ChronoUnit.DAYS.between(startDate, endDate); + BigDecimal basePrice = period.getPricePerDay().multiply(BigDecimal.valueOf(nights)); + + if (pricingMode == PricingMode.PER_GUEST) { + return basePrice.multiply(BigDecimal.valueOf(guestCount)); + } + + return basePrice; + } + + private ReservationValidationResponse buildErrorResponse(String errorCode, String errorMessage) { + return ReservationValidationResponse.newBuilder() + .setValid(false) + .setErrorCode(errorCode) + .setErrorMessage(errorMessage) + .build(); + } +} diff --git a/src/main/java/com/devoops/accommodation/repository/AvailabilityPeriodRepository.java b/src/main/java/com/devoops/accommodation/repository/AvailabilityPeriodRepository.java index 39d629e..9a3b8b2 100644 --- a/src/main/java/com/devoops/accommodation/repository/AvailabilityPeriodRepository.java +++ b/src/main/java/com/devoops/accommodation/repository/AvailabilityPeriodRepository.java @@ -41,4 +41,16 @@ List findOverlappingExcluding( @Param("endDate") LocalDate endDate, @Param("excludeId") UUID excludeId ); + + @Query(""" + SELECT ap FROM AvailabilityPeriod ap + WHERE ap.accommodationId = :accommodationId + AND ap.startDate <= :startDate + AND ap.endDate >= :endDate + """) + Optional findCoveringPeriod( + @Param("accommodationId") UUID accommodationId, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate + ); } diff --git a/src/main/proto/accommodation_internal.proto b/src/main/proto/accommodation_internal.proto new file mode 100644 index 0000000..86bc71f --- /dev/null +++ b/src/main/proto/accommodation_internal.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; +package accommodation; + +option java_multiple_files = true; +option java_package = "com.devoops.accommodation.grpc.proto"; + +service AccommodationInternalService { + rpc ValidateAndCalculatePrice(ReservationValidationRequest) returns (ReservationValidationResponse); +} + +message ReservationValidationRequest { + string accommodation_id = 1; + string start_date = 2; + string end_date = 3; + int32 guest_count = 4; +} + +message ReservationValidationResponse { + bool valid = 1; + string error_code = 2; + string error_message = 3; + string host_id = 4; + string total_price = 5; + string pricing_mode = 6; + string approval_mode = 7; + string accommodation_name = 8; +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e63e04c..b30cf27 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -45,6 +45,9 @@ minio.bucket=${MINIO_BUCKET:accommodation-photos} app.photo.max-photos-per-accommodation=20 app.photo.allowed-content-types=image/jpeg,image/png,image/webp +# gRPC Server +grpc.server.port=${GRPC_PORT:9090} + # gRPC Client grpc.client.reservation-service.address=static://${RESERVATION_GRPC_HOST:devoops-reservation-service}:${RESERVATION_GRPC_PORT:9090} grpc.client.reservation-service.negotiationType=plaintext