diff --git a/src/main/java/com/soupulsar/application/dto/request/CreatePaymentRequest.java b/src/main/java/com/soupulsar/application/dto/request/CreatePaymentRequest.java new file mode 100644 index 0000000..170f96a --- /dev/null +++ b/src/main/java/com/soupulsar/application/dto/request/CreatePaymentRequest.java @@ -0,0 +1,24 @@ +package com.soupulsar.application.dto.request; + +import com.soupulsar.domain.model.enums.PaymentMethod; +import com.soupulsar.domain.model.vo.Money; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.util.UUID; + +public record CreatePaymentRequest( + + @NotNull(message = "Session ID cannot be null") + UUID sessionId, + @NotNull(message = "Specialist ID cannot be null") + UUID specialistId, + @NotNull(message = "Client ID cannot be null") + UUID clientId, + @NotNull(message = "Price cannot be null") + Money price, + Money discount, + @NotBlank(message = "Payment method cannot be blank") + PaymentMethod paymentMethod +) { +} \ No newline at end of file diff --git a/src/main/java/com/soupulsar/application/usecase/payment/CreatePaymentUseCase.java b/src/main/java/com/soupulsar/application/usecase/payment/CreatePaymentUseCase.java new file mode 100644 index 0000000..a7778d5 --- /dev/null +++ b/src/main/java/com/soupulsar/application/usecase/payment/CreatePaymentUseCase.java @@ -0,0 +1,74 @@ +package com.soupulsar.application.usecase.payment; + +import com.soupulsar.application.dto.request.CreatePaymentRequest; +import com.soupulsar.domain.exceptions.PaymentSplitRuleNotFoundException; +import com.soupulsar.domain.exceptions.UserNotFoundException; +import com.soupulsar.domain.model.payment.Payment; +import com.soupulsar.domain.model.payment.PaymentSplitRule; +import com.soupulsar.domain.model.specialist.SpecialistProfile; +import com.soupulsar.domain.model.vo.Money; +import com.soupulsar.domain.model.vo.PaymentAmounts; +import com.soupulsar.domain.model.vo.PaymentSplit; +import com.soupulsar.domain.repository.PaymentRepository; +import com.soupulsar.domain.repository.PaymentSplitRuleRepository; +import com.soupulsar.domain.repository.SpecialistProfileRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Comparator; +import java.util.List; +import java.util.UUID; + +@Slf4j +@RequiredArgsConstructor +public class CreatePaymentUseCase { + + private final PaymentRepository paymentRepository; + private final PaymentSplitRuleRepository paymentSplitRuleRepository; + private final SpecialistProfileRepository specialistProfileRepository; + + public void execute(CreatePaymentRequest request) { + + PaymentAmounts amounts = new PaymentAmounts(request.price(), request.discount()); + PaymentSplitRule splitRule = getPaymentSplitRule(request.specialistId()); + Money platformAmount = splitRule.getPlatformPercentage().applyTo(amounts.getFinalAmount()); + Money specialistAmount = amounts.getFinalAmount().subtract(platformAmount); + PaymentSplit split = new PaymentSplit(platformAmount, specialistAmount); + + Payment payment = Payment.create( + request.sessionId(), + request.specialistId(), + request.clientId(), + amounts, + split, + request.paymentMethod() + ); + paymentRepository.save(payment); + + log.info( + "Payment created: paymentId={}, sessionId={}, specialistId={}, clientId={}, finalAmount={}", + payment.getId(), + request.sessionId(), + request.specialistId(), + request.clientId(), + amounts.getFinalAmount() + ); + } + + private PaymentSplitRule getPaymentSplitRule(UUID specialistId) { + + SpecialistProfile profile = specialistProfileRepository.findById(specialistId) + .orElseThrow(() -> new UserNotFoundException(specialistId)); + + List rules = paymentSplitRuleRepository + .findActiveApplicableRules(specialistId, profile.getSpecialistType()); + + if (rules.isEmpty()) { + throw new PaymentSplitRuleNotFoundException(); + } + + return rules.stream() + .min(Comparator.comparingInt(r -> r.getScope().getPriority())) + .orElseThrow(PaymentSplitRuleNotFoundException::new); + } +} \ No newline at end of file diff --git a/src/main/java/com/soupulsar/application/usecase/payment/CreateSessionPaymentUseCase.java b/src/main/java/com/soupulsar/application/usecase/payment/CreateSessionPaymentUseCase.java deleted file mode 100644 index 708a0fc..0000000 --- a/src/main/java/com/soupulsar/application/usecase/payment/CreateSessionPaymentUseCase.java +++ /dev/null @@ -1,75 +0,0 @@ -//package com.soupulsar.application.usecase.payment; -// -//import com.asaas.apisdk.AsaasSdk; -//import com.soupulsar.application.dto.request.AsaasCustomerRequest; -//import com.soupulsar.domain.model.payment.Payment; -//import com.soupulsar.domain.model.client.ClientProfile; -//import com.soupulsar.domain.model.session.Session; -//import com.soupulsar.domain.model.user.User; -//import com.soupulsar.domain.repository.ClientProfileRepository; -//import com.soupulsar.domain.repository.PaymentRepository; -//import com.soupulsar.domain.repository.SessionRepository; -//import com.soupulsar.domain.repository.UserRepository; -//import com.soupulsar.infrastructure.persistence.mapper.AsaasMapper; -//import lombok.RequiredArgsConstructor; -// -//import java.util.UUID; -// -//@RequiredArgsConstructor -//public class CreateSessionPaymentUseCase { -// -// private final UserRepository userRepository; -// private final ClientProfileRepository clientProfileRepository; -// private final PaymentRepository paymentRepository; -// private final SessionRepository sessionRepository; -// private final AsaasSdk asaasService; -// -// public Payment execute(UUID sessionId) { -// -// Session session = sessionRepository.findBySessionId(sessionId) -// .orElseThrow(() -> new IllegalArgumentException("Session not found")); -// User client = userRepository.findById(session.getClientId()) -// .orElseThrow(() -> new IllegalArgumentException("Client not found")); -// User specialist = userRepository.findById(session.getSpecialistId()) -// .orElseThrow(() -> new IllegalArgumentException("Specialist not found")); -// -// String asaasCustomerId = ensureAsaasCustomer(client); -// -// asaasService.payment.createNewPayment(); -// -// return null; -// -// } -// -// private String ensureAsaasCustomer(User client) { -// -// ClientProfile clientProfile = clientProfileRepository.findById(client.getUserId()) -// .orElseThrow(() -> new IllegalArgumentException("Client profile not found")); -// -// if (client.hasAsaasCustomerId()) { -// return clientProfile.getAsaasCustomerId(); -// } -// -// var response = asaasService.customer.createNewCustomer(AsaasMapper.toSdkDtoBuilder(customerRequest(client))); -// -// clientProfile.setAsaasCustomerId(response.getId()); -// clientProfileRepository.save(clientProfile); -// -// return response.getId(); -// -// } -// -// private AsaasCustomerRequest customerRequest(User client) { -// return AsaasCustomerRequest.builder() -// .name(client.getName()) -// .email(client.getEmail()) -// .cpf(client.getCpf()) -// .phone(client.getTelephone()) -// .address(client.getAddress()) -// .externalReference(client.getUserId().toString()) -// .build(); -// } -// -// private -// -//} \ No newline at end of file diff --git a/src/main/java/com/soupulsar/domain/exceptions/PaymentSplitRuleNotFoundException.java b/src/main/java/com/soupulsar/domain/exceptions/PaymentSplitRuleNotFoundException.java new file mode 100644 index 0000000..13c8528 --- /dev/null +++ b/src/main/java/com/soupulsar/domain/exceptions/PaymentSplitRuleNotFoundException.java @@ -0,0 +1,8 @@ +package com.soupulsar.domain.exceptions; + +public class PaymentSplitRuleNotFoundException extends RuntimeException { + + public PaymentSplitRuleNotFoundException() { + super("No applicable payment split rule found"); + } +} \ No newline at end of file diff --git a/src/main/java/com/soupulsar/domain/repository/PaymentSplitRuleRepository.java b/src/main/java/com/soupulsar/domain/repository/PaymentSplitRuleRepository.java index 5c7ed8a..ec0eeb6 100644 --- a/src/main/java/com/soupulsar/domain/repository/PaymentSplitRuleRepository.java +++ b/src/main/java/com/soupulsar/domain/repository/PaymentSplitRuleRepository.java @@ -1,7 +1,9 @@ package com.soupulsar.domain.repository; +import com.soupulsar.domain.model.enums.SpecialistType; import com.soupulsar.domain.model.payment.PaymentSplitRule; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -9,5 +11,6 @@ public interface PaymentSplitRuleRepository { Optional findById(UUID id); PaymentSplitRule save(PaymentSplitRule paymentSplitRule); + List findActiveApplicableRules(UUID specialistId, SpecialistType specialistType); } diff --git a/src/main/java/com/soupulsar/infrastructure/persistence/repository/PaymentSplitRuleJpaRepository.java b/src/main/java/com/soupulsar/infrastructure/persistence/repository/PaymentSplitRuleJpaRepository.java index 3ab96fb..6e50fde 100644 --- a/src/main/java/com/soupulsar/infrastructure/persistence/repository/PaymentSplitRuleJpaRepository.java +++ b/src/main/java/com/soupulsar/infrastructure/persistence/repository/PaymentSplitRuleJpaRepository.java @@ -1,9 +1,21 @@ package com.soupulsar.infrastructure.persistence.repository; +import com.soupulsar.domain.model.enums.SpecialistType; import com.soupulsar.infrastructure.persistence.entity.payment.PaymentSplitRuleEntity; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import java.util.List; import java.util.UUID; public interface PaymentSplitRuleJpaRepository extends JpaRepository { + + @Query(""" + SELECT p FROM PaymentSplitRuleEntity p + WHERE p.active = true + AND (p.specialistId = :specialistId OR p.specialistId IS NULL) + AND (p.specialistType = :specialistType OR p.specialistType IS NULL) + """) + List findActiveApplicableRules(UUID specialistId, SpecialistType specialistType); + } \ No newline at end of file diff --git a/src/main/java/com/soupulsar/infrastructure/persistence/repository/impl/PaymentSplitRuleImpl.java b/src/main/java/com/soupulsar/infrastructure/persistence/repository/impl/PaymentSplitRuleImpl.java index 829199a..3ab986a 100644 --- a/src/main/java/com/soupulsar/infrastructure/persistence/repository/impl/PaymentSplitRuleImpl.java +++ b/src/main/java/com/soupulsar/infrastructure/persistence/repository/impl/PaymentSplitRuleImpl.java @@ -1,5 +1,6 @@ package com.soupulsar.infrastructure.persistence.repository.impl; +import com.soupulsar.domain.model.enums.SpecialistType; import com.soupulsar.domain.model.payment.PaymentSplitRule; import com.soupulsar.domain.repository.PaymentSplitRuleRepository; import com.soupulsar.infrastructure.persistence.entity.payment.PaymentSplitRuleEntity; @@ -7,6 +8,7 @@ import com.soupulsar.infrastructure.persistence.repository.PaymentSplitRuleJpaRepository; import lombok.RequiredArgsConstructor; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -26,4 +28,15 @@ public PaymentSplitRule save(PaymentSplitRule paymentSplitRule) { PaymentSplitRuleEntity saved = repository.save(entity); return PaymentSplitRuleMapper.toModel(saved); } + + @Override + public List findActiveApplicableRules(UUID specialistId, SpecialistType specialistType) { + var rules = repository.findActiveApplicableRules(specialistId, specialistType); + if (rules != null && !rules.isEmpty()) { + return rules.stream() + .map(PaymentSplitRuleMapper::toModel) + .toList(); + } + return List.of(); + } } \ No newline at end of file diff --git a/src/test/java/com/soupulsar/modulith/SouPulsarModulithApplicationTests.java b/src/test/java/com/soupulsar/SouPulsarApplicationTests.java similarity index 67% rename from src/test/java/com/soupulsar/modulith/SouPulsarModulithApplicationTests.java rename to src/test/java/com/soupulsar/SouPulsarApplicationTests.java index eeed66e..3549670 100644 --- a/src/test/java/com/soupulsar/modulith/SouPulsarModulithApplicationTests.java +++ b/src/test/java/com/soupulsar/SouPulsarApplicationTests.java @@ -1,10 +1,10 @@ -package com.soupulsar.modulith; +package com.soupulsar; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest -class SouPulsarModulithApplicationTests { +class SouPulsarApplicationTests { @Test void contextLoads() { diff --git a/src/test/java/com/soupulsar/modulith/auth/application/usecase/AuthenticateUserUseCaseTest.java b/src/test/java/com/soupulsar/application/usecase/AuthenticateUserUseCaseTest.java similarity index 97% rename from src/test/java/com/soupulsar/modulith/auth/application/usecase/AuthenticateUserUseCaseTest.java rename to src/test/java/com/soupulsar/application/usecase/AuthenticateUserUseCaseTest.java index 4f8fb40..5ceef22 100644 --- a/src/test/java/com/soupulsar/modulith/auth/application/usecase/AuthenticateUserUseCaseTest.java +++ b/src/test/java/com/soupulsar/application/usecase/AuthenticateUserUseCaseTest.java @@ -1,10 +1,9 @@ -package com.soupulsar.modulith.auth.application.usecase; +package com.soupulsar.application.usecase; import com.soupulsar.application.dto.request.AuthUserRequest; import com.soupulsar.application.dto.response.AuthUserResponse; import com.soupulsar.application.security.JwtService; import com.soupulsar.application.security.PasswordHasher; -import com.soupulsar.application.usecase.AuthenticateUserUseCase; import com.soupulsar.domain.exceptions.UserNotFoundException; import com.soupulsar.domain.model.user.User; import com.soupulsar.domain.model.enums.UserRole; diff --git a/src/test/java/com/soupulsar/modulith/scheduling/application/usecase/CreateAvailabilityUseCaseTest.java b/src/test/java/com/soupulsar/application/usecase/availability/CreateAvailabilityUseCaseTest.java similarity index 96% rename from src/test/java/com/soupulsar/modulith/scheduling/application/usecase/CreateAvailabilityUseCaseTest.java rename to src/test/java/com/soupulsar/application/usecase/availability/CreateAvailabilityUseCaseTest.java index ddfb790..6e8228e 100644 --- a/src/test/java/com/soupulsar/modulith/scheduling/application/usecase/CreateAvailabilityUseCaseTest.java +++ b/src/test/java/com/soupulsar/application/usecase/availability/CreateAvailabilityUseCaseTest.java @@ -1,8 +1,7 @@ -package com.soupulsar.modulith.scheduling.application.usecase; +package com.soupulsar.application.usecase.availability; import com.soupulsar.application.dto.request.CreateAvailabilityRequest; import com.soupulsar.application.dto.response.CreateAvailabilityResponse; -import com.soupulsar.application.usecase.availability.CreateAvailabilityUseCase; import com.soupulsar.domain.model.availability.Availability; import com.soupulsar.domain.repository.AvailabilityRepository; import org.junit.jupiter.api.BeforeEach; diff --git a/src/test/java/com/soupulsar/application/usecase/payment/CreatePaymentUseCaseTest.java b/src/test/java/com/soupulsar/application/usecase/payment/CreatePaymentUseCaseTest.java new file mode 100644 index 0000000..462dc15 --- /dev/null +++ b/src/test/java/com/soupulsar/application/usecase/payment/CreatePaymentUseCaseTest.java @@ -0,0 +1,143 @@ +package com.soupulsar.application.usecase.payment; + +import com.soupulsar.application.dto.request.CreatePaymentRequest; +import com.soupulsar.domain.exceptions.PaymentSplitRuleNotFoundException; +import com.soupulsar.domain.exceptions.UserNotFoundException; +import com.soupulsar.domain.model.payment.Payment; +import com.soupulsar.domain.model.payment.PaymentSplitRule; +import com.soupulsar.domain.model.specialist.SpecialistProfile; +import com.soupulsar.domain.model.vo.Money; +import com.soupulsar.domain.model.vo.Percentage; +import com.soupulsar.domain.repository.PaymentRepository; +import com.soupulsar.domain.repository.PaymentSplitRuleRepository; +import com.soupulsar.domain.repository.SpecialistProfileRepository; +import com.soupulsar.domain.model.enums.PaymentMethod; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CreatePaymentUseCaseTest { + + @Mock + private PaymentRepository paymentRepository; + + @Mock + private PaymentSplitRuleRepository paymentSplitRuleRepository; + + @Mock + private SpecialistProfileRepository specialistProfileRepository; + + @InjectMocks + private CreatePaymentUseCase useCase; + + @Test + void ShouldSavePaymentWhenRequestIsValid() { + UUID sessionId = UUID.randomUUID(); + UUID specialistId = UUID.randomUUID(); + UUID clientId = UUID.randomUUID(); + + Money price = new Money(new BigDecimal("100.00")); + Money discount = new Money(new BigDecimal("10.00")); + CreatePaymentRequest request = new CreatePaymentRequest( + sessionId, + specialistId, + clientId, + price, + discount, + PaymentMethod.CREDIT_CARD + ); + + SpecialistProfile profile = mock(SpecialistProfile.class); + when(specialistProfileRepository.findById((specialistId))).thenReturn(Optional.of(profile)); + + PaymentSplitRule rule = mock(PaymentSplitRule.class); + Percentage percentage = mock(Percentage.class); + when(rule.getPlatformPercentage()).thenReturn(percentage); + + Money finalAmount = new Money(new BigDecimal("90.00")); + Money platformAmount = new Money(new BigDecimal("9.00")); + when(percentage.applyTo((finalAmount))).thenReturn(platformAmount); + + // return a single rule so comparator/min won't need to compare priorities + when(paymentSplitRuleRepository.findActiveApplicableRules(eq(specialistId), any())) + .thenReturn(List.of(rule)); + + useCase.execute(request); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Payment.class); + verify(paymentRepository, times(1)).save(captor.capture()); + + Payment saved = captor.getValue(); + // Basic assertions that something was saved; fields checks depend on Payment getters presence. + assertNotNull(saved); + } + + @Test + void ShouldThrowUserNotFoundWhenSpecialistNotFound() { + UUID sessionId = UUID.randomUUID(); + UUID specialistId = UUID.randomUUID(); + UUID clientId = UUID.randomUUID(); + + Money price = new Money(new BigDecimal("50.00")); + Money discount = Money.zero(); + CreatePaymentRequest request = new CreatePaymentRequest( + sessionId, + specialistId, + clientId, + price, + discount, + PaymentMethod.CREDIT_CARD + ); + + when(specialistProfileRepository.findById((specialistId))).thenReturn(Optional.empty()); + + assertThrows(UserNotFoundException.class, () -> useCase.execute(request)); + verify(paymentRepository, never()).save(any()); + } + + @Test + void ShouldThrowPaymentSplitRuleNotFoundWhenNoRules() { + UUID sessionId = UUID.randomUUID(); + UUID specialistId = UUID.randomUUID(); + UUID clientId = UUID.randomUUID(); + + Money price = new Money(new BigDecimal("70.00")); + Money discount = new Money(new BigDecimal("20.00")); + CreatePaymentRequest request = new CreatePaymentRequest( + sessionId, + specialistId, + clientId, + price, + discount, + PaymentMethod.CREDIT_CARD + ); + + SpecialistProfile profile = mock(SpecialistProfile.class); + when(specialistProfileRepository.findById((specialistId))).thenReturn(Optional.of(profile)); + + when(paymentSplitRuleRepository.findActiveApplicableRules(eq(specialistId), any())) + .thenReturn(List.of()); + + assertThrows(PaymentSplitRuleNotFoundException.class, () -> useCase.execute(request)); + verify(paymentRepository, never()).save(any()); + } +} diff --git a/src/test/java/com/soupulsar/modulith/scheduling/application/usecase/CancelSessionUseCaseTest.java b/src/test/java/com/soupulsar/application/usecase/session/CancelSessionUseCaseTest.java similarity index 91% rename from src/test/java/com/soupulsar/modulith/scheduling/application/usecase/CancelSessionUseCaseTest.java rename to src/test/java/com/soupulsar/application/usecase/session/CancelSessionUseCaseTest.java index d7bdf6d..1a3d248 100644 --- a/src/test/java/com/soupulsar/modulith/scheduling/application/usecase/CancelSessionUseCaseTest.java +++ b/src/test/java/com/soupulsar/application/usecase/session/CancelSessionUseCaseTest.java @@ -1,7 +1,6 @@ -package com.soupulsar.modulith.scheduling.application.usecase; +package com.soupulsar.application.usecase.session; import com.soupulsar.application.dto.response.SessionResponse; -import com.soupulsar.application.usecase.session.CancelSessionUseCase; import com.soupulsar.domain.model.session.Session; import com.soupulsar.domain.repository.SessionRepository; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/soupulsar/modulith/scheduling/application/usecase/ScheduleSessionUseCaseTest.java b/src/test/java/com/soupulsar/application/usecase/session/ScheduleSessionUseCaseTest.java similarity index 97% rename from src/test/java/com/soupulsar/modulith/scheduling/application/usecase/ScheduleSessionUseCaseTest.java rename to src/test/java/com/soupulsar/application/usecase/session/ScheduleSessionUseCaseTest.java index c85ce47..7533b69 100644 --- a/src/test/java/com/soupulsar/modulith/scheduling/application/usecase/ScheduleSessionUseCaseTest.java +++ b/src/test/java/com/soupulsar/application/usecase/session/ScheduleSessionUseCaseTest.java @@ -1,8 +1,7 @@ -package com.soupulsar.modulith.scheduling.application.usecase; +package com.soupulsar.application.usecase.session; import com.soupulsar.application.dto.request.ScheduleSessionRequest; import com.soupulsar.application.dto.response.ScheduleSessionResponse; -import com.soupulsar.application.usecase.session.ScheduleSessionUseCase; import com.soupulsar.domain.event.SessionScheduledEvent; import com.soupulsar.domain.model.availability.Availability; import com.soupulsar.domain.model.session.Session;