ApplyAdjustmentService.java

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

import com.github.jenkaby.bikerental.finance.PaymentMethod;
import com.github.jenkaby.bikerental.finance.application.usecase.ApplyAdjustmentUseCase;
import com.github.jenkaby.bikerental.finance.domain.model.Account;
import com.github.jenkaby.bikerental.finance.domain.model.Transaction;
import com.github.jenkaby.bikerental.finance.domain.model.TransactionRecordWithoutId;
import com.github.jenkaby.bikerental.finance.domain.model.TransactionType;
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.CustomerRef;
import com.github.jenkaby.bikerental.shared.domain.model.vo.Money;
import com.github.jenkaby.bikerental.shared.exception.InsufficientBalanceException;
import com.github.jenkaby.bikerental.shared.exception.ResourceNotFoundException;
import com.github.jenkaby.bikerental.shared.infrastructure.port.uuid.UuidGenerator;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Clock;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

@RequiredArgsConstructor
@Service
public class ApplyAdjustmentService implements ApplyAdjustmentUseCase {

    private final AccountRepository accountRepository;
    private final TransactionRepository transactionRepository;
    private final UuidGenerator uuidGenerator;
    private final Clock clock;

    @Override
    @Transactional
    public AdjustmentResult execute(ApplyAdjustmentCommand command) {
        Optional<Transaction> existing = transactionRepository
                .findByIdempotencyKeyAndCustomerId(command.idempotencyKey(), new CustomerRef(command.customerId()));
        if (existing.isPresent()) {
            Transaction t = existing.get();
            return new AdjustmentResult(t.getId(), t.getRecordedAt());
        }
        var customerAccount = accountRepository
                .findByCustomerId(new CustomerRef(command.customerId()))
                .orElseThrow(() -> new ResourceNotFoundException(Account.class, command.customerId().toString()));

        var systemAccount = accountRepository.getSystemAccount();

        var customerWallet = customerAccount.getWallet();
        var adjustmentSubLedger = systemAccount.getAdjustment();

        boolean isDeduction = command.amount().isNegative();
        Money absAmount = command.amount().abs();

        if (isDeduction && !customerAccount.isBalanceSufficient(absAmount)) {
            throw new InsufficientBalanceException(customerAccount.availableBalance(), absAmount);
        }

        TransactionRecordWithoutId debitChange;
        TransactionRecordWithoutId creditChange;

        if (isDeduction) {
            debitChange = customerWallet.debit(absAmount);
            creditChange = adjustmentSubLedger.credit(absAmount);
        } else {
            debitChange = adjustmentSubLedger.debit(absAmount);
            creditChange = customerWallet.credit(absAmount);
        }

        accountRepository.save(systemAccount);
        accountRepository.save(customerAccount);

        Instant recordedAt = clock.instant();
        UUID transactionId = uuidGenerator.generate();

        var transaction = Transaction.builder()
                .id(transactionId)
                .type(TransactionType.ADJUSTMENT)
                .paymentMethod(PaymentMethod.INTERNAL_TRANSFER)
                .amount(absAmount)
                .customerId(command.customerId())
                .operatorId(command.operatorId())
                .sourceType(null)
                .sourceId(null)
                .recordedAt(recordedAt)
                .idempotencyKey(command.idempotencyKey())
                .reason(command.reason())
                .records(List.of(
                        debitChange.toTransaction(uuidGenerator.generate()),
                        creditChange.toTransaction(uuidGenerator.generate())
                ))
                .build();

        transactionRepository.save(transaction);

        return new AdjustmentResult(transactionId, recordedAt);
    }
}