UpdateRentalService.java

package com.github.jenkaby.bikerental.rental.application.service;

import com.github.jenkaby.bikerental.customer.CustomerFacade;
import com.github.jenkaby.bikerental.equipment.EquipmentFacade;
import com.github.jenkaby.bikerental.equipment.EquipmentInfo;
import com.github.jenkaby.bikerental.finance.FinanceFacade;
import com.github.jenkaby.bikerental.rental.application.mapper.RentalCostCommandMapper;
import com.github.jenkaby.bikerental.rental.application.mapper.RentalEventMapper;
import com.github.jenkaby.bikerental.rental.application.service.validator.RequestedEquipmentValidator;
import com.github.jenkaby.bikerental.rental.application.usecase.UpdateRentalUseCase;
import com.github.jenkaby.bikerental.rental.domain.exception.HoldRequiredException;
import com.github.jenkaby.bikerental.rental.domain.exception.InvalidRentalPlannedDurationException;
import com.github.jenkaby.bikerental.rental.domain.exception.InvalidRentalUpdateException;
import com.github.jenkaby.bikerental.rental.domain.model.Rental;
import com.github.jenkaby.bikerental.rental.domain.model.RentalEquipment;
import com.github.jenkaby.bikerental.rental.domain.model.RentalStatus;
import com.github.jenkaby.bikerental.rental.domain.repository.RentalRepository;
import com.github.jenkaby.bikerental.rental.infrastructure.util.PatchValueParser;
import com.github.jenkaby.bikerental.shared.domain.event.RentalStarted;
import com.github.jenkaby.bikerental.shared.exception.ReferenceNotFoundException;
import com.github.jenkaby.bikerental.shared.exception.ResourceNotFoundException;
import com.github.jenkaby.bikerental.shared.infrastructure.messaging.EventPublisher;
import com.github.jenkaby.bikerental.tariff.TariffV2Facade;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Clock;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

@Slf4j
@Service
class UpdateRentalService implements UpdateRentalUseCase {

    private static final String RENTAL_EVENTS_EXCHANGER = "rental-events";

    private final RentalRepository rentalRepository;
    private final CustomerFacade customerFacade;
    private final EquipmentFacade equipmentFacade;
    private final TariffV2Facade tariffV2Facade;
    private final FinanceFacade financeFacade;
    private final EventPublisher eventPublisher;
    private final Clock clock;
    private final RentalEventMapper eventMapper;
    private final RentalCostCommandMapper costCommandMapper;
    private final PatchValueParser valueParser;
    private final RequestedEquipmentValidator validator;

    UpdateRentalService(
            RentalRepository rentalRepository,
            CustomerFacade customerFacade,
            EquipmentFacade equipmentFacade,
            TariffV2Facade tariffV2Facade,
            FinanceFacade financeFacade,
            EventPublisher eventPublisher,
            Clock clock,
            RentalEventMapper eventMapper,
            RentalCostCommandMapper costCommandMapper,
            PatchValueParser valueParser,
            RequestedEquipmentValidator validator) {
        this.rentalRepository = rentalRepository;
        this.customerFacade = customerFacade;
        this.equipmentFacade = equipmentFacade;
        this.tariffV2Facade = tariffV2Facade;
        this.financeFacade = financeFacade;
        this.eventPublisher = eventPublisher;
        this.clock = clock;
        this.eventMapper = eventMapper;
        this.costCommandMapper = costCommandMapper;
        this.valueParser = valueParser;
        this.validator = validator;
    }

    @Override
    @Transactional
    public Rental execute(Long rentalId, Map<String, Object> patch) {
        Rental rental = rentalRepository.findById(rentalId)
                .orElseThrow(() -> new ResourceNotFoundException(Rental.class, rentalId.toString()));
        var previousState = eventMapper.toRentalState(rental);

        if (patch.containsKey("customerId")) {
            UUID customerId = valueParser.parseUUID(patch.get("customerId"));
            customerFacade.findById(customerId)
                    .orElseThrow(() -> new ReferenceNotFoundException("Customer", customerId.toString()));
            rental.selectCustomer(customerId);
        }

        if (patch.containsKey("duration")) {
            Duration duration = valueParser.parseDuration(patch.get("duration"));
            if (duration == null) {
                throw new InvalidRentalUpdateException("Duration must be provided");
            }
            rental.setPlannedDuration(duration);
        }

        List<EquipmentInfo> equipments = new ArrayList<>();
        if (patch.containsKey("equipmentIds")) {
            List<Long> equipmentIds = valueParser.parseListOfLong(patch.get("equipmentIds"));
            List<EquipmentInfo> foundEquipments = equipmentFacade.findByIds(equipmentIds);
            validator.validateSize(equipmentIds, foundEquipments);
            var alreadyReservedOrRented = previousState.equipmentIds();
            var beingReserved = foundEquipments.stream()
                    .filter(e -> !alreadyReservedOrRented.contains(e.id()))
                    .toList();
            validator.validateAvailability(beingReserved);
            equipments.addAll(foundEquipments);

            if (rental.getPlannedDuration() == null) {
                throw new InvalidRentalPlannedDurationException(rental.getId());
            }

            var costCommand = costCommandMapper.toCommand(rental, foundEquipments);
            var costResult = tariffV2Facade.calculateRentalCost(costCommand);
            var breakdowns = costResult.equipmentBreakdowns();

            rental.clearEquipmentRentals();
            for (int i = 0; i < foundEquipments.size(); i++) {
                var equipment = foundEquipments.get(i);
                RentalEquipment rentalEquipment = RentalEquipment.assigned(
                        equipment.id(),
                        equipment.uid(),
                        equipment.typeSlug());
                rentalEquipment.setEstimatedCost(breakdowns.get(i).itemCost());
                rental.addEquipment(rentalEquipment);
            }
        }


        if (patch.containsKey("status")) {
            String newStatusStr = valueParser.parseString(patch.get("status"));
            RentalStatus newStatus = RentalStatus.valueOf(newStatusStr);
            log.info("Updating rental {} status to {}", rental, newStatus);

            if (RentalStatus.ACTIVE == newStatus) {
                startRental(rental);
            } else {
                rental.setStatus(newStatus);
            }
        }
        if (rental.getStatus() != RentalStatus.ACTIVE) {
            var currentState = eventMapper.toRentalState(rental);
            eventPublisher.publish(RENTAL_EVENTS_EXCHANGER, eventMapper.toRentalUpdated(rental, previousState, currentState));
        }

        return rentalRepository.save(rental);
    }

    private void startRental(Rental rental) {
        if (rental.getEstimatedCost().isPositive() && !financeFacade.hasHold(rental.toRentalRef())) {
            throw new HoldRequiredException(rental.getId());
        }

        // Activate rental (validations are performed in Rental.activate())
        LocalDateTime actualStartTime = LocalDateTime.now(clock);
        rental.activate(actualStartTime);

        // Publish event (inter-module)
        RentalStarted event = eventMapper.toRentalStarted(rental);
        eventPublisher.publish(RENTAL_EVENTS_EXCHANGER, event);
    }


}