CreateRentalService.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.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.CreateRentalUseCase;
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.shared.domain.CustomerRef;
import com.github.jenkaby.bikerental.shared.domain.event.RentalCreated;
import com.github.jenkaby.bikerental.shared.exception.ReferenceNotFoundException;
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 org.springframework.util.CollectionUtils;

import java.time.Clock;
import java.time.Instant;
import java.util.ArrayList;

@Slf4j
@Service
class CreateRentalService implements CreateRentalUseCase {

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

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

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

    @Override
    @Transactional
    public Rental execute(CreateRentalCommand command) {
        customerFacade.findById(command.customerId())
                .orElseThrow(() -> new ReferenceNotFoundException("Customer", command.customerId().toString()));

        if (CollectionUtils.isEmpty(command.equipmentIds())) {
            throw new IllegalArgumentException("At least one equipmentId must be provided");
        }

        var equipments = equipmentFacade.findByIds(command.equipmentIds());
        validator.validateSize(command.equipmentIds(), equipments);
        validator.validateAvailability(equipments);

        var costCommand = costCommandMapper.toCommand(command, equipments);
        var costResult = tariffV2Facade.calculateRentalCost(costCommand);
        var breakdowns = costResult.equipmentBreakdowns();

        Rental rental = Rental.builder()
                .status(RentalStatus.DRAFT)
                .customerId(command.customerId())
                .createdAt(Instant.now(clock))
                .equipments(new ArrayList<>())
                .plannedDuration(command.duration())
                .specialTariffId(command.specialTariffId())
                .specialPrice(command.specialPrice())
                .discountPercent(command.discountPercent())
                .build();

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

        Rental saved = repository.save(rental);

        if (saved.getEstimatedCost().isPositive()) {
            var holdInfo = financeFacade.holdFunds(
                    new CustomerRef(saved.getCustomerId()),
                    saved.toRentalRef(),
                    costResult.totalCost(),
                    command.operatorId());
            log.info("Funds held for rental {}: transactionId={}, heldAt={}",
                    saved.getId(), holdInfo.transactionRef().id(), holdInfo.recordedAt());
        }

        RentalCreated event = eventMapper.toRentalCreated(saved);
        eventPublisher.publish(RENTAL_EVENTS_EXCHANGER, event);
        return saved;
    }

    @Override
    @Transactional
    public Rental execute(CreateDraftCommand command) {
        var draft = repository.save(Rental.createDraft());
        RentalCreated event = eventMapper.toRentalCreated(draft);
        eventPublisher.publish(RENTAL_EVENTS_EXCHANGER, event);
        return draft;
    }

}