SettleRentalService.java
package com.github.jenkaby.bikerental.finance.application.service;
import com.github.jenkaby.bikerental.finance.PaymentMethod;
import com.github.jenkaby.bikerental.finance.application.usecase.SettleRentalUseCase;
import com.github.jenkaby.bikerental.finance.domain.model.*;
import com.github.jenkaby.bikerental.finance.domain.repository.AccountRepository;
import com.github.jenkaby.bikerental.finance.domain.repository.TransactionRepository;
import com.github.jenkaby.bikerental.shared.domain.IdempotencyKey;
import com.github.jenkaby.bikerental.shared.domain.TransactionRef;
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.port.uuid.UuidGenerator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Clock;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Slf4j
@RequiredArgsConstructor
@Service
public class SettleRentalService implements SettleRentalUseCase {
private final AccountRepository accountRepository;
private final TransactionRepository transactionRepository;
private final UuidGenerator uuidGenerator;
private final Clock clock;
@Override
@Transactional(noRollbackFor = {OverBudgetSettlementException.class})
public SettlementResult execute(SettleRentalCommand command) {
var existingCaptures = transactionRepository.findAllByRentalRefAndType(command.rentalRef(), TransactionType.CAPTURE);
if (!existingCaptures.isEmpty()) {
var captureRefs = existingCaptures.stream().map(t -> new TransactionRef(t.getId())).toList();
var releaseRef = transactionRepository
.findByRentalRefAndType(command.rentalRef(), TransactionType.RELEASE)
.map(t -> new TransactionRef(t.getId()))
.orElse(null);
return new SettlementResult(captureRefs, releaseRef, existingCaptures.getFirst().getRecordedAt());
}
if (command.finalCost().isZero()) {
log.info("No settlement needed for rental {} as final cost is zero", command.rentalRef().id());
return new SettlementResult(List.of(), null, clock.instant());
}
var customerAccount = accountRepository.findByCustomerId(command.customerRef())
.orElseThrow(() -> new ResourceNotFoundException(Account.class, command.customerRef().id().toString()));
var systemAccount = accountRepository.getSystemAccount();
var holdBalance = customerAccount.getOnHold().getBalance();
Instant now = clock.instant();
var finalCost = command.finalCost();
String sourceId = String.valueOf(command.rentalRef().id());
if (!finalCost.isMoreThan(holdBalance)) {
var captureHoldDebit = customerAccount.getOnHold().debit(finalCost);
var captureRevenueCredit = systemAccount.getRevenue().credit(finalCost);
UUID captureId = uuidGenerator.generate();
var captureTransaction = Transaction.builder()
.id(captureId)
.type(TransactionType.CAPTURE)
.paymentMethod(PaymentMethod.INTERNAL_TRANSFER)
.amount(finalCost)
.customerId(command.customerRef().id())
.operatorId(command.operatorId())
.sourceType(TransactionSourceType.RENTAL)
.sourceId(sourceId)
.recordedAt(now)
.idempotencyKey(new IdempotencyKey(uuidGenerator.generate()))
.reason(null)
.records(List.of(
captureHoldDebit.toTransaction(uuidGenerator.generate()),
captureRevenueCredit.toTransaction(uuidGenerator.generate())
))
.build();
transactionRepository.save(captureTransaction);
var excess = customerAccount.getOnHold().getBalance();
var releaseTransactionRef = commitReleaseTransaction(customerAccount, command, excess, now)
.map(t -> new TransactionRef(t.getId()))
.orElse(null);
accountRepository.save(customerAccount);
accountRepository.save(systemAccount);
return new SettlementResult(List.of(new TransactionRef(captureId)), releaseTransactionRef, now);
}
var shortfall = finalCost.subtract(holdBalance);
if (customerAccount.getWallet().getBalance().isLessThan(shortfall)) {
throw new OverBudgetSettlementException(finalCost,
holdBalance.add(customerAccount.getWallet().getBalance()));
}
var captureRefs = new ArrayList<TransactionRef>();
if (holdBalance.isPositive()) {
var holdDebit = customerAccount.getOnHold().debit(holdBalance);
var holdRevenueCredit = systemAccount.getRevenue().credit(holdBalance);
UUID holdCaptureId = uuidGenerator.generate();
transactionRepository.save(Transaction.builder()
.id(holdCaptureId)
.type(TransactionType.CAPTURE)
.paymentMethod(PaymentMethod.INTERNAL_TRANSFER)
.amount(holdBalance)
.customerId(command.customerRef().id())
.operatorId(command.operatorId())
.sourceType(TransactionSourceType.RENTAL)
.sourceId(sourceId)
.recordedAt(now)
.idempotencyKey(new IdempotencyKey(uuidGenerator.generate()))
.reason(null)
.records(List.of(
holdDebit.toTransaction(uuidGenerator.generate()),
holdRevenueCredit.toTransaction(uuidGenerator.generate())
))
.build());
captureRefs.add(new TransactionRef(holdCaptureId));
}
if (shortfall.isPositive()) {
var walletDebit = customerAccount.getWallet().debit(shortfall);
var walletRevenueCredit = systemAccount.getRevenue().credit(shortfall);
UUID walletCaptureId = uuidGenerator.generate();
transactionRepository.save(Transaction.builder()
.id(walletCaptureId)
.type(TransactionType.CAPTURE)
.paymentMethod(PaymentMethod.INTERNAL_TRANSFER)
.amount(shortfall)
.customerId(command.customerRef().id())
.operatorId(command.operatorId())
.sourceType(TransactionSourceType.RENTAL)
.sourceId(sourceId)
.recordedAt(now)
.idempotencyKey(new IdempotencyKey(uuidGenerator.generate()))
.reason(null)
.records(List.of(
walletDebit.toTransaction(uuidGenerator.generate()),
walletRevenueCredit.toTransaction(uuidGenerator.generate())
))
.build());
captureRefs.add(new TransactionRef(walletCaptureId));
}
accountRepository.save(customerAccount);
accountRepository.save(systemAccount);
return new SettlementResult(captureRefs, null, now);
}
private Optional<Transaction> commitReleaseTransaction(CustomerAccount account, SettleRentalCommand command, Money excess, Instant now) {
if (excess.isPositive()) {
var releaseHoldDebit = account.getOnHold().debit(excess);
var releaseWalletCredit = account.getWallet().credit(excess);
var releaseTransaction = Transaction.builder()
.id(uuidGenerator.generate())
.type(TransactionType.RELEASE)
.paymentMethod(PaymentMethod.INTERNAL_TRANSFER)
.amount(excess)
.customerId(command.customerRef().id())
.operatorId(command.operatorId())
.sourceType(TransactionSourceType.RENTAL)
.sourceId(String.valueOf(command.rentalRef().id()))
.recordedAt(now)
.idempotencyKey(new IdempotencyKey(uuidGenerator.generate()))
.reason(null)
.records(List.of(
releaseHoldDebit.toTransaction(uuidGenerator.generate()),
releaseWalletCredit.toTransaction(uuidGenerator.generate())
))
.build();
return Optional.of(transactionRepository.save(releaseTransaction));
}
return Optional.empty();
}
}