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
3 changes: 2 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
5 changes: 4 additions & 1 deletion environment/.local.env
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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<ReservationValidationResponse> 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<Accommodation> 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<AvailabilityPeriod> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,16 @@ List<AvailabilityPeriod> 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<AvailabilityPeriod> findCoveringPeriod(
@Param("accommodationId") UUID accommodationId,
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate
);
}
27 changes: 27 additions & 0 deletions src/main/proto/accommodation_internal.proto
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 3 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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