ReturnEquipmentService.java
package com.github.jenkaby.bikerental.rental.application.service;
import com.github.jenkaby.bikerental.finance.FinanceFacade;
import com.github.jenkaby.bikerental.finance.SettlementInfo;
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.usecase.ReturnEquipmentUseCase;
import com.github.jenkaby.bikerental.rental.domain.exception.InvalidRentalStatusException;
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.RentalEquipmentStatus;
import com.github.jenkaby.bikerental.rental.domain.model.RentalStatus;
import com.github.jenkaby.bikerental.rental.domain.repository.RentalRepository;
import com.github.jenkaby.bikerental.rental.domain.service.RentalDurationCalculator;
import com.github.jenkaby.bikerental.shared.domain.CustomerRef;
import com.github.jenkaby.bikerental.shared.domain.RentalRef;
import com.github.jenkaby.bikerental.shared.domain.event.RentalCompleted;
import com.github.jenkaby.bikerental.shared.domain.model.vo.Money;
import com.github.jenkaby.bikerental.shared.exception.OverBudgetSettlementException;
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.jspecify.annotations.NonNull;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Clock;
import java.time.LocalDateTime;
@Slf4j
@Service
class ReturnEquipmentService implements ReturnEquipmentUseCase {
private static final String RENTAL_EVENTS_EXCHANGER = "rental-events";
private final RentalRepository rentalRepository;
private final RentalDurationCalculator durationCalculator;
private final TariffV2Facade tariffV2Facade;
private final RentalCostCommandMapper costCommandMapper;
private final FinanceFacade financeFacade;
private final RentalEventMapper eventMapper;
private final EventPublisher eventPublisher;
private final Clock clock;
ReturnEquipmentService(
RentalRepository rentalRepository,
RentalDurationCalculator durationCalculator,
TariffV2Facade tariffV2Facade,
RentalCostCommandMapper costCommandMapper,
FinanceFacade financeFacade,
RentalEventMapper eventMapper,
EventPublisher eventPublisher,
Clock clock) {
this.rentalRepository = rentalRepository;
this.durationCalculator = durationCalculator;
this.tariffV2Facade = tariffV2Facade;
this.costCommandMapper = costCommandMapper;
this.financeFacade = financeFacade;
this.eventMapper = eventMapper;
this.eventPublisher = eventPublisher;
this.clock = clock;
}
@Override
@Transactional
public @NonNull ReturnEquipmentResult execute(@NonNull ReturnEquipmentCommand command) {
log.info("Processing equipment return for rentalId={}, equipmentIds={}, equipmentUids={}",
command.rentalId(), command.equipmentIds(), command.equipmentUids());
LocalDateTime returnTime = LocalDateTime.now(clock);
Rental rental = findRental(command);
if (!rental.hasActiveStatus()) {
throw new InvalidRentalStatusException(rental.getStatus(), RentalStatus.ACTIVE);
}
var durationResult = rental.calculateActualDuration(durationCalculator, returnTime);
var equipmentsToReturn = rental.equipmentsToReturn(command.getEquipmentIds(), command.getEquipmentUids(), returnTime);
var costCommand = costCommandMapper.toReturnCommand(rental, equipmentsToReturn, durationResult.billableDuration());
var costResult = tariffV2Facade.calculateRentalCost(costCommand);
var breakdowns = costResult.equipmentBreakdowns();
for (int i = 0; i < equipmentsToReturn.size(); i++) {
var equipment = equipmentsToReturn.get(i);
var breakdown = breakdowns.get(i);
equipment.setFinalCost(breakdown.itemCost());
equipment.setTariffId(breakdown.tariffId());
}
if (!rental.allEquipmentReturned()) {
Rental saved = rentalRepository.save(rental);
log.info("Partial return recorded for rental {}", saved.getId());
RentalCompleted event = eventMapper.toRentalCompleted(saved, returnTime, saved.getFinalCost());
eventPublisher.publish(RENTAL_EVENTS_EXCHANGER, event);
return new ReturnEquipmentResult(saved, null);
}
// TODO Move to rental class
Money previouslyReturnedCost = rental.getEquipments().stream()
.filter(e -> e.getStatus() == RentalEquipmentStatus.RETURNED)
.filter(e -> !equipmentsToReturn.contains(e))
.map(RentalEquipment::getFinalCost)
.reduce(Money.zero(), Money::add);
var totalFinalCost = previouslyReturnedCost.add(costResult.totalCost());
SettlementInfo settlementInfo = null;
try {
settlementInfo = financeFacade.settleRental(
CustomerRef.of(rental.getCustomerId()),
RentalRef.of(rental.getId()),
totalFinalCost,
command.operatorId()
);
rental.completeWithStatus(totalFinalCost, RentalStatus.COMPLETED);
} catch (OverBudgetSettlementException obe) {
rental.completeWithStatus(totalFinalCost, RentalStatus.DEBT);
}
Rental saved = rentalRepository.save(rental);
RentalCompleted event = eventMapper.toRentalCompleted(saved, returnTime, totalFinalCost);
eventPublisher.publish(RENTAL_EVENTS_EXCHANGER, event);
return new ReturnEquipmentResult(saved, settlementInfo);
}
private Rental findRental(ReturnEquipmentCommand command) {
return rentalRepository.findById(command.rentalId())
.orElseThrow(() -> new ResourceNotFoundException(Rental.class, command.rentalId().toString()));
}
}