This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
package ru.soune.nocopy.service.payout;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.nocopy.entity.referral.Referral;
|
||||
import ru.soune.nocopy.entity.user.User;
|
||||
import ru.soune.nocopy.exception.InsufficientFundsException;
|
||||
import ru.soune.nocopy.repository.ReferralJpaRepository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class BalanceService {
|
||||
|
||||
private final ReferralJpaRepository referralJpaRepository;
|
||||
|
||||
public boolean hasSufficientFunds(User user, BigDecimal amount) {
|
||||
Referral referral = referralJpaRepository.findByUserId(user.getId());
|
||||
BigDecimal available = BigDecimal.valueOf(referral.getAvailableIncome());
|
||||
|
||||
return available.compareTo(amount) >= 0;
|
||||
}
|
||||
|
||||
public BigDecimal getBalance(User user) {
|
||||
Referral referral = referralJpaRepository.findByUserId(user.getId());
|
||||
return BigDecimal.valueOf(referral.getAvailableIncome());
|
||||
}
|
||||
|
||||
public BigDecimal getFrozenBalance(User user) {
|
||||
Referral referral = referralJpaRepository.findByUserId(user.getId());
|
||||
return BigDecimal.valueOf(referral.getHoldBalance());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void freezeAmount(User user, BigDecimal amount) {
|
||||
Referral referral = referralJpaRepository.findByUserId(user.getId());
|
||||
BigDecimal available = BigDecimal.valueOf(referral.getAvailableIncome());
|
||||
|
||||
if (available.compareTo(amount) < 0) {
|
||||
throw new InsufficientFundsException(
|
||||
String.format("Not have amount. Amount have: %.2f", available)
|
||||
);
|
||||
}
|
||||
|
||||
BigDecimal newAvailable = available.subtract(amount);
|
||||
BigDecimal newHold = BigDecimal.valueOf(referral.getHoldBalance()).add(amount);
|
||||
|
||||
referral.setAvailableIncome(newAvailable.intValue());
|
||||
referral.setHoldBalance(newHold.intValue());
|
||||
|
||||
referralJpaRepository.save(referral);
|
||||
|
||||
log.info("Amount freeze. Have: {}, Frozen: {}", newAvailable, newHold);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void unfreezeAmount(Long userId, BigDecimal amount) {
|
||||
log.info("Unfreeze {} for user {}", amount, userId);
|
||||
|
||||
Referral referral = referralJpaRepository.findByUserId(userId);
|
||||
BigDecimal hold = BigDecimal.valueOf(referral.getHoldBalance());
|
||||
|
||||
if (hold.compareTo(amount) < 0) {
|
||||
throw new IllegalStateException(
|
||||
String.format("Not have freeze amount. Frozen: %.2f", hold)
|
||||
);
|
||||
}
|
||||
|
||||
BigDecimal newAvailable = BigDecimal.valueOf(referral.getAvailableIncome()).add(amount);
|
||||
BigDecimal newHold = hold.subtract(amount);
|
||||
|
||||
referral.setAvailableIncome(newAvailable.intValue());
|
||||
referral.setHoldBalance(newHold.intValue());
|
||||
|
||||
referralJpaRepository.save(referral);
|
||||
|
||||
log.info("Amount frozen. Have: {}, Frozen: {}", newAvailable, newHold);
|
||||
}
|
||||
|
||||
public void withdrawAmount(Long userId, BigDecimal amount) {
|
||||
Referral referral = referralJpaRepository.findByUserId(userId);
|
||||
|
||||
if (referral.getHoldBalance() < amount.intValue()) {
|
||||
throw new IllegalStateException("Not have amount");
|
||||
}
|
||||
|
||||
referral.setHoldBalance(referral.getHoldBalance() - amount.intValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package ru.soune.nocopy.service.payout;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.nocopy.dto.payout.AddBankTransferRequest;
|
||||
import ru.soune.nocopy.dto.payout.AddCardRequest;
|
||||
import ru.soune.nocopy.dto.payout.PayoutMethodDTO;
|
||||
import ru.soune.nocopy.entity.payout.BankTransferPayoutMethod;
|
||||
import ru.soune.nocopy.entity.payout.CardPayoutMethod;
|
||||
import ru.soune.nocopy.entity.payout.InternalPayoutMethod;
|
||||
import ru.soune.nocopy.entity.payout.PayoutMethod;
|
||||
import ru.soune.nocopy.entity.user.User;
|
||||
import ru.soune.nocopy.exception.DuplicateMethodException;
|
||||
import ru.soune.nocopy.repository.PayoutMethodRepository;
|
||||
import ru.soune.nocopy.repository.UserRepository;
|
||||
import ru.soune.nocopy.service.user.UserService;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PayoutMethodService {
|
||||
|
||||
private final PayoutMethodRepository payoutMethodRepository;
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Transactional
|
||||
public void initiatedStandartPayoutMethods(User user) {
|
||||
createInternalMethod(user);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InternalPayoutMethod createInternalMethod(User user) {
|
||||
if (payoutMethodRepository.findByUserAndType(user.getId(), InternalPayoutMethod.class).isPresent()) {
|
||||
return (InternalPayoutMethod) payoutMethodRepository.findByUserAndType(
|
||||
user.getId(), InternalPayoutMethod.class).get();
|
||||
}
|
||||
|
||||
InternalPayoutMethod internalMethod = new InternalPayoutMethod();
|
||||
internalMethod.setUser(user);
|
||||
internalMethod.setDefault(true);
|
||||
|
||||
return payoutMethodRepository.save(internalMethod);
|
||||
}
|
||||
|
||||
public List<PayoutMethodDTO> getUserMethods(long userId) {
|
||||
return payoutMethodRepository.findByUserId(userId)
|
||||
.stream()
|
||||
.map(this::convertToDto)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void setDefaultMethod(Long userId, Long methodId) {
|
||||
payoutMethodRepository.clearOtherDefault(userId, methodId);
|
||||
|
||||
PayoutMethod method = payoutMethodRepository.findByIdAndUserId(methodId, userId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Not found method"));
|
||||
method.setDefault(true);
|
||||
|
||||
payoutMethodRepository.save(method);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public BankTransferPayoutMethod addBankTransferMethod(Long userId, AddBankTransferRequest request) {
|
||||
User user = userRepository.findById(userId).orElseThrow();
|
||||
|
||||
BankTransferPayoutMethod bankMethod = new BankTransferPayoutMethod();
|
||||
bankMethod.setUser(user);
|
||||
bankMethod.setBankName(request.getBankName());
|
||||
bankMethod.setBic(request.getBic());
|
||||
bankMethod.setCorrespondentAccount(request.getCorrespondentAccount());
|
||||
bankMethod.setAccountNumber(request.getAccountNumber());
|
||||
bankMethod.setAccountHolder(request.getAccountHolder());
|
||||
bankMethod.setInn(request.getInn());
|
||||
bankMethod.setKpp(request.getKpp());
|
||||
bankMethod.setDefault(payoutMethodRepository.countByUserId(user.getId()) == 0);
|
||||
|
||||
return payoutMethodRepository.save(bankMethod);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CardPayoutMethod addCardMethod(Long userId, AddCardRequest request) {
|
||||
String cardHash = generateCardHash(request.getCardNumber());
|
||||
|
||||
if (payoutMethodRepository.existsByCardHash(cardHash)) {
|
||||
throw new DuplicateMethodException("Card was added");
|
||||
}
|
||||
|
||||
User user = userRepository.findById(userId).orElseThrow();
|
||||
|
||||
CardPayoutMethod cardMethod = new CardPayoutMethod();
|
||||
cardMethod.setUser(user);
|
||||
cardMethod.setCardNumber(request.getCardNumber());
|
||||
cardMethod.setCardHash(cardHash);
|
||||
cardMethod.setDefault(payoutMethodRepository.countByUserId(user.getId()) == 0);
|
||||
|
||||
CardPayoutMethod saved = payoutMethodRepository.save(cardMethod);
|
||||
log.info("Added user card: {}", user.getId());
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
public PayoutMethod getDefaultMethod(User user) {
|
||||
return payoutMethodRepository.findByUserIdAndIsDefaultTrue(user.getId())
|
||||
.orElseGet(() -> {
|
||||
List<PayoutMethod> methods = payoutMethodRepository.findByUserId(user.getId());
|
||||
if (methods.isEmpty()) {
|
||||
return createInternalMethod(user);
|
||||
}
|
||||
return methods.get(0);
|
||||
});
|
||||
}
|
||||
|
||||
public PayoutMethodDTO convertToDto(PayoutMethod method) {
|
||||
return PayoutMethodDTO.builder()
|
||||
.id(method.getId())
|
||||
.type(method.getClass().getSimpleName().replace("PayoutMethod", ""))
|
||||
.displayName(method.getDisplayName())
|
||||
.maskedDetails(method.getMaskedDetails())
|
||||
.isDefault(method.isDefault())
|
||||
.build();
|
||||
}
|
||||
|
||||
private String generateCardHash(String cardNumber) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = md.digest(cardNumber.getBytes());
|
||||
return Base64.getEncoder().encodeToString(hash);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("Failed generate hash", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package ru.soune.nocopy.service.payout;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.ReferralService;
|
||||
import ru.soune.nocopy.dto.payout.PayoutRequestDTO;
|
||||
import ru.soune.nocopy.entity.payout.PayoutMethod;
|
||||
import ru.soune.nocopy.entity.payout.PayoutRequest;
|
||||
import ru.soune.nocopy.entity.payout.PayoutRequestStatus;
|
||||
import ru.soune.nocopy.entity.payout.PayoutType;
|
||||
import ru.soune.nocopy.entity.user.User;
|
||||
import ru.soune.nocopy.exception.InsufficientFundsException;
|
||||
import ru.soune.nocopy.exception.InvalidPayoutMethodException;
|
||||
import ru.soune.nocopy.exception.PendingPayoutExistsException;
|
||||
import ru.soune.nocopy.repository.PayoutMethodRepository;
|
||||
import ru.soune.nocopy.repository.PayoutRequestRepository;
|
||||
import ru.soune.nocopy.repository.UserRepository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PayoutRequestService {
|
||||
private final PayoutRequestRepository payoutRequestRepository;
|
||||
|
||||
private final PayoutMethodRepository payoutMethodRepository;
|
||||
|
||||
private final BalanceService balanceService;
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
private static final BigDecimal MIN_PAYOUT_AMOUNT = new BigDecimal("1000.00");
|
||||
|
||||
private static final List<PayoutRequestStatus> BLOCKING_STATUSES = Arrays.asList(PayoutRequestStatus.PENDING,
|
||||
PayoutRequestStatus.PROCESSING);
|
||||
|
||||
@Transactional
|
||||
public PayoutRequest createPayoutRequest(Long userId, PayoutRequestDTO request) {
|
||||
log.info("Create request for money {}: sum {}", userId, request.getAmount());
|
||||
|
||||
validatePayoutAmount(request.getAmount());
|
||||
|
||||
User user = userRepository.findById(userId).orElseThrow();
|
||||
|
||||
if (!balanceService.hasSufficientFunds(user, request.getAmount())) {
|
||||
throw new InsufficientFundsException(String.format("Not have money for payout. Have: %.2f",
|
||||
balanceService.getBalance(user)));
|
||||
}
|
||||
|
||||
if (hasActivePayoutRequests(user)) {
|
||||
throw new PendingPayoutExistsException("Have active payout. Wait for end.");
|
||||
}
|
||||
|
||||
PayoutMethod payoutMethod = payoutMethodRepository
|
||||
.findByIdAndUserId(request.getPayoutMethodId(), user.getId())
|
||||
.orElseThrow(() -> new InvalidPayoutMethodException("Not found payout method"));
|
||||
|
||||
PayoutType payoutType = determinePayoutType(payoutMethod);
|
||||
|
||||
PayoutRequest payoutRequest = PayoutRequest.builder()
|
||||
.user(user)
|
||||
.payoutMethod(payoutMethod)
|
||||
.amount(request.getAmount())
|
||||
.status(PayoutRequestStatus.PENDING)
|
||||
.payoutType(payoutType)
|
||||
.build();
|
||||
|
||||
PayoutRequest savedRequest = payoutRequestRepository.save(payoutRequest);
|
||||
|
||||
balanceService.freezeAmount(user, request.getAmount());
|
||||
|
||||
log.info("Payout requset created: ID={}, user={}, sum={}, status={}",
|
||||
savedRequest.getId(), user.getId(), request.getAmount(), savedRequest.getStatus());
|
||||
|
||||
return savedRequest;
|
||||
}
|
||||
|
||||
private void validatePayoutAmount(BigDecimal amount) {
|
||||
if (amount == null) {
|
||||
throw new IllegalArgumentException("Sum is null");
|
||||
}
|
||||
|
||||
if (amount.compareTo(MIN_PAYOUT_AMOUNT) < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Min sum for payout: %.2f rub.", MIN_PAYOUT_AMOUNT)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean hasActivePayoutRequests(User user) {
|
||||
return payoutRequestRepository.existsByUserIdAndStatusIn(user.getId(), BLOCKING_STATUSES);
|
||||
}
|
||||
|
||||
private PayoutType determinePayoutType(PayoutMethod payoutMethod) {
|
||||
String className = payoutMethod.getClass().getSimpleName();
|
||||
|
||||
switch (className) {
|
||||
case "CardPayoutMethod":
|
||||
return PayoutType.CARD;
|
||||
case "BankTransferPayoutMethod":
|
||||
return PayoutType.BANK_TRANSFER;
|
||||
case "InternalPayoutMethod":
|
||||
return PayoutType.INTERNAL;
|
||||
default:
|
||||
throw new InvalidPayoutMethodException("Unknown method");
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PayoutRequest> getUserPayoutRequests(Long userId) {
|
||||
return payoutRequestRepository.findByUserIdOrderByCreatedAtDesc(userId);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PayoutRequest getPayoutRequest(Long userId, Long requestId) {
|
||||
return payoutRequestRepository.findByIdAndUserId(requestId, userId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Payout not found"));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PayoutRequest cancelPayoutRequest(Long userId, Long requestId) {
|
||||
PayoutRequest request = getPayoutRequest(userId, requestId);
|
||||
|
||||
if (request.getStatus() != PayoutRequestStatus.PENDING) {
|
||||
throw new IllegalStateException(
|
||||
"Can't cnacel: " + request.getStatus()
|
||||
);
|
||||
}
|
||||
|
||||
request.setStatus(PayoutRequestStatus.CANCELLED);
|
||||
|
||||
balanceService.unfreezeAmount(userId, request.getAmount());
|
||||
|
||||
return payoutRequestRepository.save(request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PayoutRequest processRequest(Long requestId) {
|
||||
PayoutRequest request = payoutRequestRepository.findById(requestId)
|
||||
.orElseThrow();
|
||||
|
||||
if (request.getStatus() != PayoutRequestStatus.PENDING) {
|
||||
throw new IllegalStateException("Can't proceed in status: " + request.getStatus());
|
||||
}
|
||||
|
||||
request.setStatus(PayoutRequestStatus.COMPLETED);
|
||||
request.setProcessAt(LocalDateTime.now());
|
||||
|
||||
balanceService.withdrawAmount(request.getUser().getId(), request.getAmount());
|
||||
|
||||
return payoutRequestRepository.save(request);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user