diff --git a/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/domain/entities/Payment.java b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/domain/entities/Payment.java new file mode 100644 index 0000000..c1abe59 --- /dev/null +++ b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/domain/entities/Payment.java @@ -0,0 +1,56 @@ +package br.com.fiap.grupo30.fastfood.payments_api.domain.entities; + +import br.com.fiap.grupo30.fastfood.payments_api.domain.enums.PaymentStatus; +import br.com.fiap.grupo30.fastfood.payments_api.infrastructure.persistence.entities.PaymentEntity; +import br.com.fiap.grupo30.fastfood.payments_api.presentation.dtos.PaymentDto; +import java.util.Objects; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter(AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Payment { + private Long id; + private PaymentStatus status; + private Double amount; + + public static Payment create(Long orderId) { + return new Payment(orderId, PaymentStatus.PENDING, 0.0); + } + + public void setPaymentCollected(Double paymentCollectedAmount) { + this.setStatus(PaymentStatus.COLLECTED); + this.setAmount(paymentCollectedAmount); + } + + public void setPaymentRejected() { + this.setStatus(PaymentStatus.REJECTED); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Payment payment)) return false; + return Objects.equals(id, payment.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + public PaymentDto toDto() { + return new PaymentDto(status, amount); + } + + public PaymentEntity toPersistence() { + return new PaymentEntity(id, status, amount); + } + + public static Payment fromPersistence(PaymentEntity payment) { + return new Payment(payment.getOrderId(), payment.getStatus(), payment.getAmount()); + } +} diff --git a/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/domain/enums/PaymentStatus.java b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/domain/enums/PaymentStatus.java new file mode 100644 index 0000000..43846fd --- /dev/null +++ b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/domain/enums/PaymentStatus.java @@ -0,0 +1,7 @@ +package br.com.fiap.grupo30.fastfood.payments_api.domain.enums; + +public enum PaymentStatus { + PENDING, + REJECTED, + COLLECTED, +} diff --git a/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/domain/repositories/PaymentRepository.java b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/domain/repositories/PaymentRepository.java new file mode 100644 index 0000000..230af33 --- /dev/null +++ b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/domain/repositories/PaymentRepository.java @@ -0,0 +1,35 @@ +package br.com.fiap.grupo30.fastfood.payments_api.domain.repositories; + +import br.com.fiap.grupo30.fastfood.payments_api.domain.entities.Payment; +import br.com.fiap.grupo30.fastfood.payments_api.infrastructure.persistence.repositories.PaymentJpaRepository; +import java.util.Optional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +public class PaymentRepository { + + @Autowired private PaymentJpaRepository paymentJpaRepository; + + @Transactional(readOnly = true) + public Optional findByOrderId(Long orderId) { + return paymentJpaRepository + .findById(orderId) + .map(Payment::fromPersistence) + .or(() -> Optional.empty()); + } + + @Transactional() + public Optional findByOrderIdForUpdate(Long orderId) { + return paymentJpaRepository + .findById(orderId) + .map(Payment::fromPersistence) + .or(() -> Optional.empty()); + } + + @Transactional + public Payment save(Payment payment) { + return Payment.fromPersistence(paymentJpaRepository.save(payment.toPersistence())); + } +} diff --git a/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/domain/usecases/CollectPaymentViaCashUseCase.java b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/domain/usecases/CollectPaymentViaCashUseCase.java new file mode 100644 index 0000000..fed433c --- /dev/null +++ b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/domain/usecases/CollectPaymentViaCashUseCase.java @@ -0,0 +1,20 @@ +package br.com.fiap.grupo30.fastfood.payments_api.domain.usecases; + +import br.com.fiap.grupo30.fastfood.payments_api.domain.entities.Payment; +import br.com.fiap.grupo30.fastfood.payments_api.domain.repositories.PaymentRepository; +import br.com.fiap.grupo30.fastfood.payments_api.presentation.dtos.PaymentDto; +import org.springframework.stereotype.Component; + +@Component +public class CollectPaymentViaCashUseCase { + + public PaymentDto execute( + PaymentRepository paymentRepository, Long orderId, Double paidAmount) { + Payment payment = + paymentRepository.findByOrderIdForUpdate(orderId).orElse(Payment.create(orderId)); + + payment.setPaymentCollected(paidAmount); + + return paymentRepository.save(payment).toDto(); + } +} diff --git a/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/infrastructure/auth/RequireAdminUserRole.java b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/infrastructure/auth/RequireAdminUserRole.java new file mode 100644 index 0000000..0fe7ec3 --- /dev/null +++ b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/infrastructure/auth/RequireAdminUserRole.java @@ -0,0 +1,10 @@ +package br.com.fiap.grupo30.fastfood.payments_api.infrastructure.auth; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface RequireAdminUserRole {} diff --git a/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/infrastructure/auth/RequireAdminUserRoleAspect.java b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/infrastructure/auth/RequireAdminUserRoleAspect.java new file mode 100644 index 0000000..0753cda --- /dev/null +++ b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/infrastructure/auth/RequireAdminUserRoleAspect.java @@ -0,0 +1,61 @@ +package br.com.fiap.grupo30.fastfood.payments_api.infrastructure.auth; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Base64; +import java.util.List; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@Aspect +@Component +public class RequireAdminUserRoleAspect { + + private final ObjectMapper objectMapper = new ObjectMapper(); + private static String ADMIN_ROLE = "admin-group"; + private static String BEARER_TYPE = "Bearer"; + private static Integer JWT_PARTS = 3; + + @Before("@annotation(RequireAdminUserRole)") + public void checkAdminRole() throws Exception { + ServletRequestAttributes attrs = + (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + HttpServletRequest request = attrs.getRequest(); + HttpServletResponse response = attrs.getResponse(); + + String authorizationHeader = request.getHeader("Authorization"); + + if (authorizationHeader == null || !authorizationHeader.startsWith(BEARER_TYPE)) { + response.sendError( + HttpServletResponse.SC_UNAUTHORIZED, "Missing or invalid Authorization header"); + return; + } + + String jwtToken = authorizationHeader.substring(7); + String[] tokenParts = jwtToken.split("\\."); + if (tokenParts.length != JWT_PARTS) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid JWT token structure"); + return; + } + + String payload = new String(Base64.getDecoder().decode(tokenParts[1])); + JsonNode jsonNode = objectMapper.readTree(payload); + JsonNode groupsNode = jsonNode.get("cognito:groups"); + if (groupsNode != null && groupsNode.isArray()) { + List groups = objectMapper.convertValue(groupsNode, List.class); + if (!groups.contains(ADMIN_ROLE)) { + response.sendError( + HttpServletResponse.SC_UNAUTHORIZED, "User does not have admin role"); + return; + } + } else { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "No cognito:groups found"); + return; + } + } +} diff --git a/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/infrastructure/persistence/entities/PaymentEntity.java b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/infrastructure/persistence/entities/PaymentEntity.java new file mode 100644 index 0000000..424f000 --- /dev/null +++ b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/infrastructure/persistence/entities/PaymentEntity.java @@ -0,0 +1,60 @@ +package br.com.fiap.grupo30.fastfood.payments_api.infrastructure.persistence.entities; + +import br.com.fiap.grupo30.fastfood.payments_api.domain.enums.PaymentStatus; +import jakarta.persistence.*; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import java.time.Instant; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode +@Entity +@Table(name = "tb_payment") +public class PaymentEntity { + @Id + @Column(nullable = false) + private Long orderId; + + @Enumerated(EnumType.STRING) + private PaymentStatus status; + + @Column(nullable = false) + @Min(0) + @Max(10_000) + private Double amount; + + @Column(columnDefinition = "TIMESTAMP WITHOUT TIME ZONE", updatable = false) + private Instant createdAt; + + @Column(columnDefinition = "TIMESTAMP WITHOUT TIME ZONE") + private Instant updatedAt; + + @Column(columnDefinition = "TIMESTAMP WITHOUT TIME ZONE") + private Instant deletedAt; + + public PaymentEntity(Long orderId, PaymentStatus status, Double amount) { + this.orderId = orderId; + this.status = status; + this.amount = amount; + } + + @PrePersist + protected void prePersist() { + createdAt = Instant.now(); + } + + @PreUpdate + protected void preUpdate() { + updatedAt = Instant.now(); + } + + @PreRemove + protected void preRemove() { + deletedAt = Instant.now(); + } +} diff --git a/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/infrastructure/persistence/repositories/PaymentJpaRepository.java b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/infrastructure/persistence/repositories/PaymentJpaRepository.java new file mode 100644 index 0000000..6644742 --- /dev/null +++ b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/infrastructure/persistence/repositories/PaymentJpaRepository.java @@ -0,0 +1,8 @@ +package br.com.fiap.grupo30.fastfood.payments_api.infrastructure.persistence.repositories; + +import br.com.fiap.grupo30.fastfood.payments_api.infrastructure.persistence.entities.PaymentEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PaymentJpaRepository extends JpaRepository {} diff --git a/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/presentation/controllers/PaymentCollectionController.java b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/presentation/controllers/PaymentCollectionController.java new file mode 100644 index 0000000..fa79362 --- /dev/null +++ b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/presentation/controllers/PaymentCollectionController.java @@ -0,0 +1,36 @@ +package br.com.fiap.grupo30.fastfood.payments_api.presentation.controllers; + +import br.com.fiap.grupo30.fastfood.payments_api.domain.repositories.PaymentRepository; +import br.com.fiap.grupo30.fastfood.payments_api.domain.usecases.CollectPaymentViaCashUseCase; +import br.com.fiap.grupo30.fastfood.payments_api.infrastructure.auth.RequireAdminUserRole; +import br.com.fiap.grupo30.fastfood.payments_api.presentation.dtos.CollectPaymentViaCashRequestDto; +import br.com.fiap.grupo30.fastfood.payments_api.presentation.dtos.PaymentDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(value = "/payments") +@Tag(name = "Payment collection controller", description = "Manage payment collection") +public class PaymentCollectionController { + + @Autowired private CollectPaymentViaCashUseCase collectPaymentViaCashUseCase; + @Autowired private PaymentRepository paymentRepository; + + @RequireAdminUserRole() + @PostMapping(value = "/{orderId}/collect/cash") + @Operation(summary = "Collect payment by cash") + public ResponseEntity collectPaymentByCash( + @PathVariable Long orderId, @RequestBody CollectPaymentViaCashRequestDto request) { + PaymentDto payment = + this.collectPaymentViaCashUseCase.execute( + paymentRepository, orderId, request.getAmount()); + return ResponseEntity.ok().body(payment); + } +} diff --git a/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/presentation/dtos/CollectPaymentViaCashRequestDto.java b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/presentation/dtos/CollectPaymentViaCashRequestDto.java new file mode 100644 index 0000000..82304cf --- /dev/null +++ b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/presentation/dtos/CollectPaymentViaCashRequestDto.java @@ -0,0 +1,14 @@ +package br.com.fiap.grupo30.fastfood.payments_api.presentation.dtos; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class CollectPaymentViaCashRequestDto { + @Min(0) + @Max(10_000) + private Double amount; +} diff --git a/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/presentation/dtos/PaymentDto.java b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/presentation/dtos/PaymentDto.java new file mode 100644 index 0000000..fa344bd --- /dev/null +++ b/src/main/java/br/com/fiap/grupo30/fastfood/payments_api/presentation/dtos/PaymentDto.java @@ -0,0 +1,12 @@ +package br.com.fiap.grupo30.fastfood.payments_api.presentation.dtos; + +import br.com.fiap.grupo30.fastfood.payments_api.domain.enums.PaymentStatus; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class PaymentDto { + private PaymentStatus status; + private Double amount; +}