diff --git a/build.gradle b/build.gradle index 6cde39d..d8e87d5 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,8 @@ dependencies { implementation 'com.github.lolgab:snunit-autowire_native0.4.0-M2_2.11:0.0.4' implementation 'jakarta.validation:jakarta.validation-api:3.0.2' + implementation 'com.mercadopago:sdk-java:2.5.0' + // Swagger documentation implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' diff --git a/src/main/java/com/cuoco/adapter/in/controller/UserPaymentsControllerAdapter.java b/src/main/java/com/cuoco/adapter/in/controller/UserPaymentsControllerAdapter.java new file mode 100644 index 0000000..4755b8e --- /dev/null +++ b/src/main/java/com/cuoco/adapter/in/controller/UserPaymentsControllerAdapter.java @@ -0,0 +1,52 @@ +package com.cuoco.adapter.in.controller; + +import com.cuoco.adapter.in.controller.model.UserPaymentResponse; +import com.cuoco.application.port.in.CreateUserPaymentCommand; +import com.cuoco.application.port.in.ProcessUserPaymentCommand; +import com.cuoco.application.usecase.model.UserPayment; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@Slf4j +@RestController +@RequestMapping("/payments") +@RequiredArgsConstructor +public class UserPaymentsControllerAdapter { + + private final CreateUserPaymentCommand createUserPaymentCommand; + private final ProcessUserPaymentCommand processUserPaymentCommand; + + @PostMapping + public ResponseEntity init() { + log.info("Executing POST for init user subscription payment"); + + UserPayment userPayment = createUserPaymentCommand.execute(); + UserPaymentResponse response = UserPaymentResponse.fromDomain(userPayment); + + log.info("User subscription payment response: {}", response); + return ResponseEntity.ok(response); + } + + + @PostMapping("/webhook") + public ResponseEntity processPayment(@RequestBody Map payload) { + log.info("Executing POST for payment webhook: {}", payload); + + ProcessUserPaymentCommand.Command command = ProcessUserPaymentCommand.Command.builder() + .payload(payload) + .build(); + + processUserPaymentCommand.execute(command); + + log.info("User payment webhook processed successfully"); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/cuoco/adapter/in/controller/model/SubscriptionResponse.java b/src/main/java/com/cuoco/adapter/in/controller/model/SubscriptionResponse.java new file mode 100644 index 0000000..e63db28 --- /dev/null +++ b/src/main/java/com/cuoco/adapter/in/controller/model/SubscriptionResponse.java @@ -0,0 +1,4 @@ +package com.cuoco.adapter.in.controller.model; + +public class SubscriptionResponse { +} diff --git a/src/main/java/com/cuoco/adapter/in/controller/model/UserPaymentResponse.java b/src/main/java/com/cuoco/adapter/in/controller/model/UserPaymentResponse.java new file mode 100644 index 0000000..7dbca1e --- /dev/null +++ b/src/main/java/com/cuoco/adapter/in/controller/model/UserPaymentResponse.java @@ -0,0 +1,33 @@ +package com.cuoco.adapter.in.controller.model; + +import com.cuoco.application.usecase.model.UserPayment; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@JsonIgnoreProperties(ignoreUnknown = true) +public class UserPaymentResponse { + + private String externalId; + private String externalReference; + private String checkoutUrl; + + public static UserPaymentResponse fromDomain(UserPayment userPayment) { + return UserPaymentResponse.builder() + .externalId(userPayment.getExternalId()) + .externalReference(userPayment.getExternalReference()) + .checkoutUrl(userPayment.getCheckoutUrl()) + .build(); + } +} diff --git a/src/main/java/com/cuoco/adapter/in/security/JwtAuthenticationFilterAdapter.java b/src/main/java/com/cuoco/adapter/in/security/JwtAuthenticationFilterAdapter.java index 18cf56e..13d33bd 100644 --- a/src/main/java/com/cuoco/adapter/in/security/JwtAuthenticationFilterAdapter.java +++ b/src/main/java/com/cuoco/adapter/in/security/JwtAuthenticationFilterAdapter.java @@ -77,6 +77,7 @@ protected boolean shouldNotFilter(HttpServletRequest request) { || matcher.match("/cook-levels", request.getRequestURI()) || matcher.match("/meal-types", request.getRequestURI()) || matcher.match("/preparation-times", request.getRequestURI()) + || matcher.match("/payments/webhook", request.getRequestURI()) || matcher.match("/v3/api-docs/**", request.getRequestURI()) || matcher.match("/swagger-ui/**", request.getRequestURI()) || matcher.match("/swagger-ui.html", request.getRequestURI()); diff --git a/src/main/java/com/cuoco/adapter/out/hibernate/CreateUserPaymentDatabaseRepositoryAdapter.java b/src/main/java/com/cuoco/adapter/out/hibernate/CreateUserPaymentDatabaseRepositoryAdapter.java new file mode 100644 index 0000000..a7bb771 --- /dev/null +++ b/src/main/java/com/cuoco/adapter/out/hibernate/CreateUserPaymentDatabaseRepositoryAdapter.java @@ -0,0 +1,37 @@ +package com.cuoco.adapter.out.hibernate; + +import com.cuoco.adapter.out.hibernate.model.UserHibernateModel; +import com.cuoco.adapter.out.hibernate.model.UserPaymentsHibernateModel; +import com.cuoco.adapter.out.hibernate.repository.CreateUserPaymentHibernateRepositoryAdapter; +import com.cuoco.application.port.out.CreateUserPaymentRepository; +import com.cuoco.application.usecase.model.UserPayment; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Repository; + +@Slf4j +@Repository +@Qualifier("repository") +@RequiredArgsConstructor +public class CreateUserPaymentDatabaseRepositoryAdapter implements CreateUserPaymentRepository { + + private final CreateUserPaymentHibernateRepositoryAdapter createUserPaymentHibernateRepositoryAdapter; + + @Override + public UserPayment execute(UserPayment userPayment) { + log.info("Executing create user payment in database"); + + UserPaymentsHibernateModel userPaymentToSave = UserPaymentsHibernateModel.fromDomain(userPayment); + + UserPaymentsHibernateModel savedUserPayment = createUserPaymentHibernateRepositoryAdapter.save(userPaymentToSave); + + log.info("Saved user payment with ID {}", savedUserPayment.getId()); + + UserPayment response = savedUserPayment.toDomain(); + + response.setUser(userPayment.getUser()); + + return response; + } +} diff --git a/src/main/java/com/cuoco/adapter/out/hibernate/GetAllPaymentStatusDatabaseRepositoryAdapter.java b/src/main/java/com/cuoco/adapter/out/hibernate/GetAllPaymentStatusDatabaseRepositoryAdapter.java new file mode 100644 index 0000000..42b8de5 --- /dev/null +++ b/src/main/java/com/cuoco/adapter/out/hibernate/GetAllPaymentStatusDatabaseRepositoryAdapter.java @@ -0,0 +1,28 @@ +package com.cuoco.adapter.out.hibernate; + +import com.cuoco.adapter.out.hibernate.model.PaymentStatusHibernateModel; +import com.cuoco.adapter.out.hibernate.repository.GetAllPaymentStatusHibernateRepositoryAdapter; +import com.cuoco.application.port.out.GetAllPaymentStatusRepository; +import com.cuoco.application.usecase.model.PaymentStatus; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Slf4j +@Repository +@RequiredArgsConstructor +public class GetAllPaymentStatusDatabaseRepositoryAdapter implements GetAllPaymentStatusRepository { + + private final GetAllPaymentStatusHibernateRepositoryAdapter getAllPaymentStatusHibernateRepositoryAdapter; + + @Override + public List getAll() { + log.info("Get all payment statuses from database"); + + List response = getAllPaymentStatusHibernateRepositoryAdapter.findAll(); + + return response.stream().map(PaymentStatusHibernateModel::toDomain).toList(); + } +} diff --git a/src/main/java/com/cuoco/adapter/out/hibernate/GetUserPaymentByExternalReferenceDatabaseRepositoryAdapter.java b/src/main/java/com/cuoco/adapter/out/hibernate/GetUserPaymentByExternalReferenceDatabaseRepositoryAdapter.java new file mode 100644 index 0000000..988a0c9 --- /dev/null +++ b/src/main/java/com/cuoco/adapter/out/hibernate/GetUserPaymentByExternalReferenceDatabaseRepositoryAdapter.java @@ -0,0 +1,36 @@ +package com.cuoco.adapter.out.hibernate; + +import com.cuoco.adapter.exception.NotFoundException; +import com.cuoco.adapter.out.hibernate.model.UserPaymentsHibernateModel; +import com.cuoco.adapter.out.hibernate.repository.GetUserPaymentByExternalReferenceHibernateRepositoryAdapter; +import com.cuoco.application.port.out.GetUserPaymentByExternalReferenceRepository; +import com.cuoco.application.usecase.model.UserPayment; +import com.cuoco.shared.model.ErrorDescription; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Slf4j +@Repository +@RequiredArgsConstructor +public class GetUserPaymentByExternalReferenceDatabaseRepositoryAdapter implements GetUserPaymentByExternalReferenceRepository { + + private final GetUserPaymentByExternalReferenceHibernateRepositoryAdapter getUserPaymentByExternalReferenceHibernateRepositoryAdapter; + + @Override + public UserPayment execute(String externalReference) { + log.info("Executing get user payment by external reference {}", externalReference); + + Optional maybeResponse = getUserPaymentByExternalReferenceHibernateRepositoryAdapter.findByExternalReference(externalReference); + + if(!maybeResponse.isPresent()) { + log.info("Not found user payment with external reference {}", externalReference); + throw new NotFoundException(ErrorDescription.NOT_FOUND.getValue()); + } + + UserPaymentsHibernateModel response = maybeResponse.get(); + return response.toDomain(); + } +} diff --git a/src/main/java/com/cuoco/adapter/out/hibernate/UpdateUserPaymentDatabaseRepositoryAdapter.java b/src/main/java/com/cuoco/adapter/out/hibernate/UpdateUserPaymentDatabaseRepositoryAdapter.java new file mode 100644 index 0000000..eafebc8 --- /dev/null +++ b/src/main/java/com/cuoco/adapter/out/hibernate/UpdateUserPaymentDatabaseRepositoryAdapter.java @@ -0,0 +1,40 @@ +package com.cuoco.adapter.out.hibernate; + +import com.cuoco.adapter.out.hibernate.model.UserHibernateModel; +import com.cuoco.adapter.out.hibernate.model.UserPaymentsHibernateModel; +import com.cuoco.adapter.out.hibernate.repository.CreateUserPaymentHibernateRepositoryAdapter; +import com.cuoco.application.port.out.CreateUserPaymentRepository; +import com.cuoco.application.port.out.UpdateUserPaymentRepository; +import com.cuoco.application.usecase.model.UserPayment; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Repository; + +@Slf4j +@Repository +@Qualifier("repository") +@RequiredArgsConstructor +public class UpdateUserPaymentDatabaseRepositoryAdapter implements UpdateUserPaymentRepository { + + private final CreateUserPaymentHibernateRepositoryAdapter createUserPaymentHibernateRepositoryAdapter; + + @Override + public UserPayment execute(UserPayment userPayment) { + log.info("Executing update user payment in database"); + + UserPaymentsHibernateModel userPaymentToUpdate = UserPaymentsHibernateModel.fromDomain(userPayment); + + userPaymentToUpdate.setUser(UserHibernateModel.builder().id(userPayment.getUser().getId()).build()); + + UserPaymentsHibernateModel updatedUserPayment = createUserPaymentHibernateRepositoryAdapter.save(userPaymentToUpdate); + + log.info("Updated user payment with ID {}", updatedUserPayment.getId()); + + UserPayment response = updatedUserPayment.toDomain(); + + response.setUser(userPayment.getUser()); + + return response; + } +} diff --git a/src/main/java/com/cuoco/adapter/out/hibernate/model/PaymentStatusHibernateModel.java b/src/main/java/com/cuoco/adapter/out/hibernate/model/PaymentStatusHibernateModel.java new file mode 100644 index 0000000..d110762 --- /dev/null +++ b/src/main/java/com/cuoco/adapter/out/hibernate/model/PaymentStatusHibernateModel.java @@ -0,0 +1,38 @@ +package com.cuoco.adapter.out.hibernate.model; + +import com.cuoco.application.usecase.model.PaymentStatus; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity(name = "payment_status") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PaymentStatusHibernateModel { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + private String description; + + public static PaymentStatusHibernateModel fromDomain(PaymentStatus paymentStatus) { + return PaymentStatusHibernateModel.builder() + .id(paymentStatus.getId()) + .description(paymentStatus.getDescription()) + .build(); + } + + public PaymentStatus toDomain() { + return PaymentStatus.builder() + .id(id) + .description(description) + .build(); + } +} diff --git a/src/main/java/com/cuoco/adapter/out/hibernate/model/PlanConfigurationHibernateModel.java b/src/main/java/com/cuoco/adapter/out/hibernate/model/PlanConfigurationHibernateModel.java new file mode 100644 index 0000000..ba7a44e --- /dev/null +++ b/src/main/java/com/cuoco/adapter/out/hibernate/model/PlanConfigurationHibernateModel.java @@ -0,0 +1,51 @@ +package com.cuoco.adapter.out.hibernate.model; + +import com.cuoco.application.usecase.model.PlanConfiguration; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Entity(name = "plan_configuration") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PlanConfigurationHibernateModel { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + private String title; + private String description; + private Integer quantity; + private BigDecimal price; + private String currency; + + public static PlanConfigurationHibernateModel fromDomain(PlanConfiguration planConfiguration) { + return PlanConfigurationHibernateModel.builder() + .title(planConfiguration.getTitle()) + .description(planConfiguration.getDescription()) + .quantity(planConfiguration.getQuantity()) + .price(planConfiguration.getPrice()) + .currency(planConfiguration.getCurrency()) + .build(); + } + + public PlanConfiguration toDomain() { + return PlanConfiguration.builder() + .title(title) + .description(description) + .quantity(quantity) + .price(price) + .currency(currency) + .build(); + } +} diff --git a/src/main/java/com/cuoco/adapter/out/hibernate/model/PlanHibernateModel.java b/src/main/java/com/cuoco/adapter/out/hibernate/model/PlanHibernateModel.java index 540f1e4..80e8540 100644 --- a/src/main/java/com/cuoco/adapter/out/hibernate/model/PlanHibernateModel.java +++ b/src/main/java/com/cuoco/adapter/out/hibernate/model/PlanHibernateModel.java @@ -5,6 +5,8 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -22,10 +24,15 @@ public class PlanHibernateModel { private Integer id; private String description; + @OneToOne + @JoinColumn(name = "configuration_id") + private PlanConfigurationHibernateModel configuration; + public static PlanHibernateModel fromDomain(Plan plan) { return PlanHibernateModel.builder() .id(plan.getId()) .description(plan.getDescription()) + .configuration(plan.getConfiguration() != null ? PlanConfigurationHibernateModel.fromDomain(plan.getConfiguration()) : null) .build(); } @@ -33,6 +40,7 @@ public Plan toDomain() { return Plan.builder() .id(id) .description(description) + .configuration(configuration != null ? configuration.toDomain() : null) .build(); } } \ No newline at end of file diff --git a/src/main/java/com/cuoco/adapter/out/hibernate/model/UserHibernateModel.java b/src/main/java/com/cuoco/adapter/out/hibernate/model/UserHibernateModel.java index e5176d2..d002f32 100644 --- a/src/main/java/com/cuoco/adapter/out/hibernate/model/UserHibernateModel.java +++ b/src/main/java/com/cuoco/adapter/out/hibernate/model/UserHibernateModel.java @@ -1,7 +1,7 @@ package com.cuoco.adapter.out.hibernate.model; -import com.cuoco.application.usecase.model.Allergy; import com.cuoco.application.usecase.model.User; +import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; @@ -41,6 +41,9 @@ public class UserHibernateModel { private LocalDateTime updatedAt; private LocalDateTime deletedAt; + @OneToOne(mappedBy = "user", cascade = CascadeType.ALL) + private UserPreferencesHibernateModel preferences; + @ManyToMany @JoinTable( name = "user_allergies", @@ -76,6 +79,22 @@ public class UserHibernateModel { @OneToMany(mappedBy = "user") private List calendars; + public static UserHibernateModel fromDomain(User user) { + return UserHibernateModel.builder() + .id(user.getId()) + .name(user.getName()) + .email(user.getEmail()) + .password(user.getPassword()) + .plan(PlanHibernateModel.fromDomain(user.getPlan())) + .active(user.getActive()) + .preferences(UserPreferencesHibernateModel.fromDomain(user.getPreferences())) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .dietaryNeeds(user.getDietaryNeeds().stream().map(DietaryNeedHibernateModel::fromDomain).toList()) + .allergies(user.getAllergies().stream().map(AllergyHibernateModel::fromDomain).toList()) + .build(); + } + public User toDomain() { return User.builder() .id(id) diff --git a/src/main/java/com/cuoco/adapter/out/hibernate/model/UserPaymentsHibernateModel.java b/src/main/java/com/cuoco/adapter/out/hibernate/model/UserPaymentsHibernateModel.java new file mode 100644 index 0000000..7e9bc8f --- /dev/null +++ b/src/main/java/com/cuoco/adapter/out/hibernate/model/UserPaymentsHibernateModel.java @@ -0,0 +1,72 @@ +package com.cuoco.adapter.out.hibernate.model; + +import com.cuoco.application.usecase.model.User; +import com.cuoco.application.usecase.model.UserPayment; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity(name = "user_payments") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserPaymentsHibernateModel { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne + @JoinColumn(name = "user_id", referencedColumnName = "id") + private UserHibernateModel user; + + @OneToOne + @JoinColumn(name = "to_plan_id") + private PlanHibernateModel toPlan; + + @OneToOne + @JoinColumn(name = "status_id") + private PaymentStatusHibernateModel status; + + private String externalId; + private String externalReference; + private String checkoutUrl; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private LocalDateTime deletedAt; + + public static UserPaymentsHibernateModel fromDomain(UserPayment userPayment) { + return UserPaymentsHibernateModel.builder() + .id(userPayment.getId()) + .user(UserHibernateModel.fromDomain(userPayment.getUser())) + .toPlan(PlanHibernateModel.fromDomain(userPayment.getPlan())) + .status(PaymentStatusHibernateModel.fromDomain(userPayment.getStatus())) + .externalId(userPayment.getExternalId()) + .externalReference(userPayment.getExternalReference()) + .checkoutUrl(userPayment.getCheckoutUrl()) + .build(); + } + + public UserPayment toDomain() { + return UserPayment.builder() + .id(id) + .user(user.toDomain()) + .plan(toPlan.toDomain()) + .status(status.toDomain()) + .externalId(externalId) + .externalReference(externalReference) + .checkoutUrl(checkoutUrl) + .build(); + } + +} diff --git a/src/main/java/com/cuoco/adapter/out/hibernate/model/UserPreferencesHibernateModel.java b/src/main/java/com/cuoco/adapter/out/hibernate/model/UserPreferencesHibernateModel.java index 2ac3218..c0e9ce8 100644 --- a/src/main/java/com/cuoco/adapter/out/hibernate/model/UserPreferencesHibernateModel.java +++ b/src/main/java/com/cuoco/adapter/out/hibernate/model/UserPreferencesHibernateModel.java @@ -36,6 +36,14 @@ public class UserPreferencesHibernateModel { @JoinColumn(name = "diet_id", referencedColumnName = "id") private DietHibernateModel diet; + public static UserPreferencesHibernateModel fromDomain(UserPreferences userPreferences) { + return UserPreferencesHibernateModel.builder() + .id(userPreferences.getId()) + .cookLevel(CookLevelHibernateModel.fromDomain(userPreferences.getCookLevel())) + .diet(DietHibernateModel.fromDomain(userPreferences.getDiet())) + .build(); + } + public UserPreferences toDomain() { return UserPreferences.builder() .id(id) diff --git a/src/main/java/com/cuoco/adapter/out/hibernate/repository/CreateUserPaymentHibernateRepositoryAdapter.java b/src/main/java/com/cuoco/adapter/out/hibernate/repository/CreateUserPaymentHibernateRepositoryAdapter.java new file mode 100644 index 0000000..57e6afe --- /dev/null +++ b/src/main/java/com/cuoco/adapter/out/hibernate/repository/CreateUserPaymentHibernateRepositoryAdapter.java @@ -0,0 +1,7 @@ +package com.cuoco.adapter.out.hibernate.repository; + +import com.cuoco.adapter.out.hibernate.model.UserPaymentsHibernateModel; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CreateUserPaymentHibernateRepositoryAdapter extends JpaRepository { +} diff --git a/src/main/java/com/cuoco/adapter/out/hibernate/repository/GetAllPaymentStatusHibernateRepositoryAdapter.java b/src/main/java/com/cuoco/adapter/out/hibernate/repository/GetAllPaymentStatusHibernateRepositoryAdapter.java new file mode 100644 index 0000000..2183d21 --- /dev/null +++ b/src/main/java/com/cuoco/adapter/out/hibernate/repository/GetAllPaymentStatusHibernateRepositoryAdapter.java @@ -0,0 +1,7 @@ +package com.cuoco.adapter.out.hibernate.repository; + +import com.cuoco.adapter.out.hibernate.model.PaymentStatusHibernateModel; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface GetAllPaymentStatusHibernateRepositoryAdapter extends JpaRepository { +} diff --git a/src/main/java/com/cuoco/adapter/out/hibernate/repository/GetUserPaymentByExternalReferenceHibernateRepositoryAdapter.java b/src/main/java/com/cuoco/adapter/out/hibernate/repository/GetUserPaymentByExternalReferenceHibernateRepositoryAdapter.java new file mode 100644 index 0000000..5a14ae5 --- /dev/null +++ b/src/main/java/com/cuoco/adapter/out/hibernate/repository/GetUserPaymentByExternalReferenceHibernateRepositoryAdapter.java @@ -0,0 +1,12 @@ +package com.cuoco.adapter.out.hibernate.repository; + +import com.cuoco.adapter.out.hibernate.model.UserPaymentsHibernateModel; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface GetUserPaymentByExternalReferenceHibernateRepositoryAdapter extends JpaRepository { + + Optional findByExternalReference(String externalReference); + +} diff --git a/src/main/java/com/cuoco/adapter/out/mercadopago/CreateUserPaymentMercadoPagoRepositoryAdapter.java b/src/main/java/com/cuoco/adapter/out/mercadopago/CreateUserPaymentMercadoPagoRepositoryAdapter.java new file mode 100644 index 0000000..d26c5ea --- /dev/null +++ b/src/main/java/com/cuoco/adapter/out/mercadopago/CreateUserPaymentMercadoPagoRepositoryAdapter.java @@ -0,0 +1,126 @@ +package com.cuoco.adapter.out.mercadopago; + +import com.cuoco.adapter.exception.NotAvailableException; +import com.cuoco.application.port.out.CreateUserPaymentRepository; +import com.cuoco.application.usecase.model.PaymentStatus; +import com.cuoco.application.usecase.model.Plan; +import com.cuoco.application.usecase.model.PlanConfiguration; +import com.cuoco.application.usecase.model.User; +import com.cuoco.application.usecase.model.UserPayment; +import com.cuoco.shared.config.mercadopago.MercadoPagoConfig; +import com.cuoco.shared.model.ErrorDescription; +import com.cuoco.shared.utils.Constants; +import com.cuoco.shared.utils.PaymentConstants; +import com.mercadopago.client.preference.PreferenceBackUrlsRequest; +import com.mercadopago.client.preference.PreferenceClient; +import com.mercadopago.client.preference.PreferenceItemRequest; +import com.mercadopago.client.preference.PreferenceRequest; +import com.mercadopago.exceptions.MPApiException; +import com.mercadopago.exceptions.MPException; +import com.mercadopago.resources.preference.Preference; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.UUID; + +@Slf4j +@Component +@Qualifier("provider") +@RequiredArgsConstructor +public class CreateUserPaymentMercadoPagoRepositoryAdapter implements CreateUserPaymentRepository { + + private static final String EXTERNAL_REFERENCE_PREFIX = "CUOCO_PRO_UPGRADE"; + private static final String HOST_DOMAIN = "cuoco.com.ar"; + private static final String API_CONTEXT = "/api"; + + private final HttpServletRequest request; + private final MercadoPagoConfig config; + + @Override + public UserPayment execute(UserPayment userPayment) { + + com.mercadopago.MercadoPagoConfig.setAccessToken(config.getAccessToken()); + + User user = userPayment.getUser(); + Plan plan = userPayment.getPlan(); + + log.info("Executing MercadoPago preference payment for user ID {} and plan ID {}", user.getId(), plan.getId()); + + try { + PreferenceRequest request = buildPreference(userPayment); + + PreferenceClient client = new PreferenceClient(); + + Preference preference = client.create(request); + + UserPayment response = buildResponse(userPayment, preference); + + log.info("Payment created with external ID {}", response.getExternalId()); + + return response; + + } catch (MPException e) { + log.error("Error while creating preference in MercadoPago SDK", e); + throw new NotAvailableException(ErrorDescription.NOT_AVAILABLE.getValue()); + } catch (MPApiException e) { + log.error("Failed to create preference payment with MercadoPago API", e); + throw new NotAvailableException(ErrorDescription.NOT_AVAILABLE.getValue()); + } + } + + private String generateExternalReference(Long userId) { + return EXTERNAL_REFERENCE_PREFIX + .concat(Constants.UNDERSCORE.getValue()) + .concat(userId.toString()) + .concat(Constants.UNDERSCORE.getValue()) + .concat(UUID.randomUUID().toString().substring(0, 8)); + } + + private PreferenceRequest buildPreference(UserPayment userPayment) { + return PreferenceRequest.builder() + .items(List.of(buildItems(userPayment.getPlan()))) + .externalReference(generateExternalReference(userPayment.getUser().getId())) + .backUrls(buildBackUrls()) + .autoReturn("approved") + .build(); + } + + private PreferenceBackUrlsRequest buildBackUrls() { + + String baseUrl = request.getRequestURL().toString().replace(request.getRequestURI(), Constants.EMPTY.getValue()); + + return PreferenceBackUrlsRequest.builder() + .success(baseUrl + config.getCallbacks().getSuccess()) + .pending(baseUrl + config.getCallbacks().getPending()) + .failure(baseUrl + config.getCallbacks().getFailure()) + .build(); + } + + private PreferenceItemRequest buildItems(Plan plan) { + PlanConfiguration planConfiguration = plan.getConfiguration(); + + return PreferenceItemRequest.builder() + .title(planConfiguration.getTitle()) + .description(planConfiguration.getDescription()) + .quantity(planConfiguration.getQuantity()) + .unitPrice(planConfiguration.getPrice()) + .currencyId(planConfiguration.getCurrency()) + .build(); + } + + private UserPayment buildResponse(UserPayment request, Preference preference) { + return UserPayment.builder() + .user(request.getUser()) + .plan(request.getPlan()) + .status(PaymentStatus.builder().id(PaymentConstants.STATUS_PENDING.getValue()).build()) + .externalId(preference.getId()) + .checkoutUrl(preference.getInitPoint()) + .externalReference(preference.getExternalReference()) + .build(); + + } +} diff --git a/src/main/java/com/cuoco/adapter/out/mercadopago/ProcessUserPaymentMercadoPagoRepositoryAdapter.java b/src/main/java/com/cuoco/adapter/out/mercadopago/ProcessUserPaymentMercadoPagoRepositoryAdapter.java new file mode 100644 index 0000000..4287759 --- /dev/null +++ b/src/main/java/com/cuoco/adapter/out/mercadopago/ProcessUserPaymentMercadoPagoRepositoryAdapter.java @@ -0,0 +1,46 @@ +package com.cuoco.adapter.out.mercadopago; + +import com.cuoco.adapter.exception.UnauthorizedException; +import com.cuoco.application.port.out.ProcessUserPaymentRepository; +import com.cuoco.application.usecase.model.PaymentStatus; +import com.cuoco.application.usecase.model.UserPayment; +import com.cuoco.shared.config.mercadopago.MercadoPagoConfig; +import com.cuoco.shared.model.ErrorDescription; +import com.mercadopago.client.payment.PaymentClient; +import com.mercadopago.exceptions.MPApiException; +import com.mercadopago.exceptions.MPException; +import com.mercadopago.resources.payment.Payment; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +@Slf4j +@Repository +@RequiredArgsConstructor +public class ProcessUserPaymentMercadoPagoRepositoryAdapter implements ProcessUserPaymentRepository { + + private final MercadoPagoConfig config; + + @Override + public UserPayment execute(String paymentId) { + + com.mercadopago.MercadoPagoConfig.setAccessToken(config.getAccessToken()); + + try { + PaymentClient client = new PaymentClient(); + Payment payment = client.get(Long.parseLong(paymentId)); + + UserPayment response = UserPayment.builder() + .status(PaymentStatus.builder().description(payment.getStatus()).build()) + .externalReference(payment.getExternalReference()) + .build(); + + log.info("Payment info received with status={} and external_reference={}", response.getStatus(), response.getExternalReference()); + + return response; + } catch (MPException | MPApiException e) { + log.error("Error fetching payment details from MercadoPago", e); + throw new UnauthorizedException(ErrorDescription.NOT_AVAILABLE.getValue()); + } + } +} diff --git a/src/main/java/com/cuoco/application/port/in/CreateUserPaymentCommand.java b/src/main/java/com/cuoco/application/port/in/CreateUserPaymentCommand.java new file mode 100644 index 0000000..352f851 --- /dev/null +++ b/src/main/java/com/cuoco/application/port/in/CreateUserPaymentCommand.java @@ -0,0 +1,7 @@ +package com.cuoco.application.port.in; + +import com.cuoco.application.usecase.model.UserPayment; + +public interface CreateUserPaymentCommand { + UserPayment execute(); +} diff --git a/src/main/java/com/cuoco/application/port/in/ProcessUserPaymentCommand.java b/src/main/java/com/cuoco/application/port/in/ProcessUserPaymentCommand.java new file mode 100644 index 0000000..d31a50b --- /dev/null +++ b/src/main/java/com/cuoco/application/port/in/ProcessUserPaymentCommand.java @@ -0,0 +1,17 @@ +package com.cuoco.application.port.in; + +import lombok.Builder; +import lombok.Data; + +import java.util.Map; + +public interface ProcessUserPaymentCommand { + void execute(Command command); + + @Data + @Builder + class Command { + private String secret; + private Map payload; + } +} diff --git a/src/main/java/com/cuoco/application/port/out/CreateUserPaymentRepository.java b/src/main/java/com/cuoco/application/port/out/CreateUserPaymentRepository.java new file mode 100644 index 0000000..09e2e4b --- /dev/null +++ b/src/main/java/com/cuoco/application/port/out/CreateUserPaymentRepository.java @@ -0,0 +1,8 @@ +package com.cuoco.application.port.out; + + +import com.cuoco.application.usecase.model.UserPayment; + +public interface CreateUserPaymentRepository { + UserPayment execute(UserPayment userPayment); +} diff --git a/src/main/java/com/cuoco/application/port/out/GetAllPaymentStatusRepository.java b/src/main/java/com/cuoco/application/port/out/GetAllPaymentStatusRepository.java new file mode 100644 index 0000000..d580ce2 --- /dev/null +++ b/src/main/java/com/cuoco/application/port/out/GetAllPaymentStatusRepository.java @@ -0,0 +1,9 @@ +package com.cuoco.application.port.out; + +import com.cuoco.application.usecase.model.PaymentStatus; + +import java.util.List; + +public interface GetAllPaymentStatusRepository { + List getAll(); +} diff --git a/src/main/java/com/cuoco/application/port/out/GetUserPaymentByExternalReferenceRepository.java b/src/main/java/com/cuoco/application/port/out/GetUserPaymentByExternalReferenceRepository.java new file mode 100644 index 0000000..6339a48 --- /dev/null +++ b/src/main/java/com/cuoco/application/port/out/GetUserPaymentByExternalReferenceRepository.java @@ -0,0 +1,7 @@ +package com.cuoco.application.port.out; + +import com.cuoco.application.usecase.model.UserPayment; + +public interface GetUserPaymentByExternalReferenceRepository { + UserPayment execute(String externalReference); +} diff --git a/src/main/java/com/cuoco/application/port/out/ProcessUserPaymentRepository.java b/src/main/java/com/cuoco/application/port/out/ProcessUserPaymentRepository.java new file mode 100644 index 0000000..1546eea --- /dev/null +++ b/src/main/java/com/cuoco/application/port/out/ProcessUserPaymentRepository.java @@ -0,0 +1,7 @@ +package com.cuoco.application.port.out; + +import com.cuoco.application.usecase.model.UserPayment; + +public interface ProcessUserPaymentRepository { + UserPayment execute(String paymentId); +} diff --git a/src/main/java/com/cuoco/application/port/out/UpdateUserPaymentRepository.java b/src/main/java/com/cuoco/application/port/out/UpdateUserPaymentRepository.java new file mode 100644 index 0000000..0cdfc75 --- /dev/null +++ b/src/main/java/com/cuoco/application/port/out/UpdateUserPaymentRepository.java @@ -0,0 +1,7 @@ +package com.cuoco.application.port.out; + +import com.cuoco.application.usecase.model.UserPayment; + +public interface UpdateUserPaymentRepository { + UserPayment execute(UserPayment userPayment); +} diff --git a/src/main/java/com/cuoco/application/usecase/CreateUserPaymentUseCase.java b/src/main/java/com/cuoco/application/usecase/CreateUserPaymentUseCase.java new file mode 100644 index 0000000..3c3650a --- /dev/null +++ b/src/main/java/com/cuoco/application/usecase/CreateUserPaymentUseCase.java @@ -0,0 +1,62 @@ +package com.cuoco.application.usecase; + +import com.cuoco.application.exception.BadRequestException; +import com.cuoco.application.port.in.CreateUserPaymentCommand; +import com.cuoco.application.port.out.CreateUserPaymentRepository; +import com.cuoco.application.port.out.GetPlanByIdRepository; +import com.cuoco.application.usecase.domainservice.UserDomainService; +import com.cuoco.application.usecase.model.Plan; +import com.cuoco.application.usecase.model.User; +import com.cuoco.application.usecase.model.UserPayment; +import com.cuoco.shared.model.ErrorDescription; +import com.cuoco.shared.utils.PlanConstants; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class CreateUserPaymentUseCase implements CreateUserPaymentCommand { + + private final UserDomainService userDomainService; + private final GetPlanByIdRepository getPlanByIdRepository; + private final CreateUserPaymentRepository createUserPaymentProvider; + private final CreateUserPaymentRepository createUserPaymentRepository; + + public CreateUserPaymentUseCase( + UserDomainService userDomainService, + GetPlanByIdRepository getPlanByIdRepository, + @Qualifier("provider") CreateUserPaymentRepository createUserPaymentProvider, + @Qualifier("repository") CreateUserPaymentRepository createUserPaymentRepository + ) { + this.userDomainService = userDomainService; + this.getPlanByIdRepository = getPlanByIdRepository; + this.createUserPaymentProvider = createUserPaymentProvider; + this.createUserPaymentRepository = createUserPaymentRepository; + } + + @Override + public UserPayment execute() { + User user = userDomainService.getCurrentUser(); + + if(user.getPlan().getId() == PlanConstants.PRO.getValue()) { + log.info("User already has a PRO plan"); + throw new BadRequestException(ErrorDescription.USER_HAS_PRO_PLAN.getValue()); + } + + Plan plan = getPlanByIdRepository.execute(PlanConstants.PRO.getValue()); + + UserPayment userPayment = createUserPaymentProvider.execute(buildUserPayment(user, plan)); + + createUserPaymentRepository.execute(userPayment); + + return userPayment; + } + + private UserPayment buildUserPayment(User user, Plan plan) { + return UserPayment.builder() + .user(user) + .plan(plan) + .build(); + } +} diff --git a/src/main/java/com/cuoco/application/usecase/ProcessUserPaymentUseCase.java b/src/main/java/com/cuoco/application/usecase/ProcessUserPaymentUseCase.java new file mode 100644 index 0000000..604e596 --- /dev/null +++ b/src/main/java/com/cuoco/application/usecase/ProcessUserPaymentUseCase.java @@ -0,0 +1,77 @@ +package com.cuoco.application.usecase; + +import com.cuoco.application.port.in.ProcessUserPaymentCommand; +import com.cuoco.application.port.out.GetAllPaymentStatusRepository; +import com.cuoco.application.port.out.GetUserByIdRepository; +import com.cuoco.application.port.out.GetUserPaymentByExternalReferenceRepository; +import com.cuoco.application.port.out.ProcessUserPaymentRepository; +import com.cuoco.application.port.out.UpdateUserPaymentRepository; +import com.cuoco.application.port.out.UpdateUserRepository; +import com.cuoco.application.usecase.model.PaymentStatus; +import com.cuoco.application.usecase.model.User; +import com.cuoco.application.usecase.model.UserPayment; +import com.cuoco.shared.utils.PaymentConstants; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ProcessUserPaymentUseCase implements ProcessUserPaymentCommand { + + private static final String PAYMENT = "payment"; + private static final String APPROVED = "approved"; + + private final ProcessUserPaymentRepository processPaymentProvider; + private final GetUserPaymentByExternalReferenceRepository getUserPaymentByExternalReferenceRepository; + private final UpdateUserPaymentRepository updateUserPaymentRepository; + private final UpdateUserRepository updateUserRepository; + private final GetUserByIdRepository getUserByIdRepository; + + @Override + public void execute(Command command) { + log.info("Execute user process payment with command {}", command); + + String type = (String) command.getPayload().get("type"); + String paymentId = getPaymentId(command.getPayload()); + + if (PAYMENT.equalsIgnoreCase(type) && (paymentId != null)) { + UserPayment receivedPayment = processPaymentProvider.execute(paymentId); + + if (receivedPayment.getStatus().getDescription().equalsIgnoreCase(APPROVED)) { + + UserPayment userPayment = getUserPaymentByExternalReferenceRepository.execute(receivedPayment.getExternalReference()); + + User user = getUserByIdRepository.execute(userPayment.getUser().getId()); + + userPayment.setUser(user); + + userPayment.setStatus(PaymentStatus.builder().id(PaymentConstants.STATUS_APPROVED.getValue()).build()); + + updateUserPlan(userPayment); + + updateUserPaymentRepository.execute(userPayment); + } + } + } + + private void updateUserPlan(UserPayment userPayment) { + User user = userPayment.getUser(); + user.setPlan(userPayment.getPlan()); + + updateUserRepository.execute(user); + } + + private String getPaymentId(Map payload) { + if(payload.containsKey("data") && payload.get("data") != null) { + return String.valueOf(((Map) payload.get("data")).get("id")); + } + + return null; + } +} diff --git a/src/main/java/com/cuoco/application/usecase/model/PaymentStatus.java b/src/main/java/com/cuoco/application/usecase/model/PaymentStatus.java new file mode 100644 index 0000000..1e7f702 --- /dev/null +++ b/src/main/java/com/cuoco/application/usecase/model/PaymentStatus.java @@ -0,0 +1,13 @@ +package com.cuoco.application.usecase.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +@AllArgsConstructor +public class PaymentStatus implements Parametric { + private Integer id; + private String description; +} diff --git a/src/main/java/com/cuoco/application/usecase/model/Plan.java b/src/main/java/com/cuoco/application/usecase/model/Plan.java index 7ced45b..34d6821 100644 --- a/src/main/java/com/cuoco/application/usecase/model/Plan.java +++ b/src/main/java/com/cuoco/application/usecase/model/Plan.java @@ -10,4 +10,6 @@ public class Plan implements Parametric { private Integer id; private String description; + + private PlanConfiguration configuration; } diff --git a/src/main/java/com/cuoco/application/usecase/model/PlanConfiguration.java b/src/main/java/com/cuoco/application/usecase/model/PlanConfiguration.java new file mode 100644 index 0000000..35571a0 --- /dev/null +++ b/src/main/java/com/cuoco/application/usecase/model/PlanConfiguration.java @@ -0,0 +1,18 @@ +package com.cuoco.application.usecase.model; + +import lombok.Builder; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +@Builder +public class PlanConfiguration { + + private String title; + private String description; + private Integer quantity; + private BigDecimal price; + private String currency; + +} diff --git a/src/main/java/com/cuoco/application/usecase/model/Subscription.java b/src/main/java/com/cuoco/application/usecase/model/Subscription.java new file mode 100644 index 0000000..ec2f28c --- /dev/null +++ b/src/main/java/com/cuoco/application/usecase/model/Subscription.java @@ -0,0 +1,9 @@ +package com.cuoco.application.usecase.model; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class Subscription { +} diff --git a/src/main/java/com/cuoco/application/usecase/model/UserPayment.java b/src/main/java/com/cuoco/application/usecase/model/UserPayment.java new file mode 100644 index 0000000..f4a8455 --- /dev/null +++ b/src/main/java/com/cuoco/application/usecase/model/UserPayment.java @@ -0,0 +1,18 @@ +package com.cuoco.application.usecase.model; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class UserPayment { + + private Long id; + private User user; + private Plan plan; + private PaymentStatus status; + private String externalId; + private String externalReference; + private String checkoutUrl; + +} diff --git a/src/main/java/com/cuoco/shared/config/mercadopago/MercadoPagoBrandingConfig.java b/src/main/java/com/cuoco/shared/config/mercadopago/MercadoPagoBrandingConfig.java new file mode 100644 index 0000000..dd8bc8b --- /dev/null +++ b/src/main/java/com/cuoco/shared/config/mercadopago/MercadoPagoBrandingConfig.java @@ -0,0 +1,14 @@ +package com.cuoco.shared.config.mercadopago; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "mercado-pago.branding") +public class MercadoPagoBrandingConfig { + private String primaryColor; + private String secondaryColor; + private boolean showMercadoPagoBranding; +} diff --git a/src/main/java/com/cuoco/shared/config/mercadopago/MercadoPagoCallbacksConfig.java b/src/main/java/com/cuoco/shared/config/mercadopago/MercadoPagoCallbacksConfig.java new file mode 100644 index 0000000..00a5b9f --- /dev/null +++ b/src/main/java/com/cuoco/shared/config/mercadopago/MercadoPagoCallbacksConfig.java @@ -0,0 +1,15 @@ +package com.cuoco.shared.config.mercadopago; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "mercado-pago.callbacks") +public class MercadoPagoCallbacksConfig { + private String success; + private String pending; + private String failure; +} + diff --git a/src/main/java/com/cuoco/shared/config/mercadopago/MercadoPagoConfig.java b/src/main/java/com/cuoco/shared/config/mercadopago/MercadoPagoConfig.java new file mode 100644 index 0000000..13ec8a1 --- /dev/null +++ b/src/main/java/com/cuoco/shared/config/mercadopago/MercadoPagoConfig.java @@ -0,0 +1,16 @@ +package com.cuoco.shared.config.mercadopago; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "mercado-pago") +public class MercadoPagoConfig { + + private String accessToken; + private MercadoPagoCallbacksConfig callbacks; + private MercadoPagoBrandingConfig branding; + +} \ No newline at end of file diff --git a/src/main/java/com/cuoco/shared/config/security/SecurityConfiguration.java b/src/main/java/com/cuoco/shared/config/security/SecurityConfiguration.java index 3c63221..e0b8fa9 100644 --- a/src/main/java/com/cuoco/shared/config/security/SecurityConfiguration.java +++ b/src/main/java/com/cuoco/shared/config/security/SecurityConfiguration.java @@ -41,6 +41,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/preparation-times", "/dietary-needs", "/allergies", + "/payments/webhook", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html" diff --git a/src/main/java/com/cuoco/shared/model/ErrorDescription.java b/src/main/java/com/cuoco/shared/model/ErrorDescription.java index f992299..c14cbfa 100644 --- a/src/main/java/com/cuoco/shared/model/ErrorDescription.java +++ b/src/main/java/com/cuoco/shared/model/ErrorDescription.java @@ -26,6 +26,7 @@ public enum ErrorDescription { USER_NOT_EXISTS("El usuario ingresado no existe"), USER_DUPLICATED("El usuario ya existe"), USER_NOT_ACTIVATED("El usuario no esta activo"), + USER_HAS_PRO_PLAN("El usuario ya tiene el plan PRO"), NO_AUTH_TOKEN("El token no esta presente"), INVALID_TOKEN("El token ingresado no es válido"), INVALID_CREDENTIALS("Las credenciales no son válidas"), diff --git a/src/main/java/com/cuoco/shared/utils/Constants.java b/src/main/java/com/cuoco/shared/utils/Constants.java index 3d2e924..9cbc1a0 100644 --- a/src/main/java/com/cuoco/shared/utils/Constants.java +++ b/src/main/java/com/cuoco/shared/utils/Constants.java @@ -2,6 +2,7 @@ public enum Constants { + UNDERSCORE("_"), COMMA(","), SLASH("/"), DOT("."), diff --git a/src/main/java/com/cuoco/shared/utils/PaymentConstants.java b/src/main/java/com/cuoco/shared/utils/PaymentConstants.java new file mode 100644 index 0000000..b3a81cf --- /dev/null +++ b/src/main/java/com/cuoco/shared/utils/PaymentConstants.java @@ -0,0 +1,17 @@ +package com.cuoco.shared.utils; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum PaymentConstants { + + STATUS_PENDING(1), + STATUS_APPROVED(2), + STATUS_IN_PROCESS(3), + STATUS_REJECTED(4); + + private final int value; + +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b6eb693..d36710a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -40,6 +40,16 @@ email: connectiontimeout: 5000 timeout: 5000 writetimeout: 5000 +mercado-pago: + access-token: ${MP_ACCESS_TOKEN} + callbacks: + success: ${MP_CALLBACK_SUCCESS:/payment/success} + pending: ${MP_CALLBACK_PENDING:/payment/pending} + failure: ${MP_CALLBACK_FAILURE:/payment/failure} + branding: + primary-color: ${MP_BRANDING_PRIMARY_COLOR:#FF6B35} + secondary-color: ${MP_BRANDING_SECONDARY_COLOR:#FFA500} + show-mercado-pago-branding: ${MP_BRANDING_ENABLE_MP_BRANDING:false} shared: email: no-reply: @@ -56,4 +66,8 @@ shared: base-path: ${RECIPE_IMAGES_BASE_PATH} meal-preps: size: ${MEAL_PREP_DEFAULT_SIZE:1} - recipes-size: ${RECIPES_SIZE_PER_MEAL_PREP:3} \ No newline at end of file + recipes-size: ${RECIPES_SIZE_PER_MEAL_PREP:3} + + + + diff --git a/src/main/resources/sql/ddl/01_user_tables.sql b/src/main/resources/sql/ddl/01_user_tables.sql index d78651b..a9fc466 100644 --- a/src/main/resources/sql/ddl/01_user_tables.sql +++ b/src/main/resources/sql/ddl/01_user_tables.sql @@ -20,6 +20,26 @@ CREATE TABLE diets ); CREATE TABLE plans +( + `id` int NOT NULL AUTO_INCREMENT, + `description` varchar(255) DEFAULT NULL, + `configuration_id` int DEFAULT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `FK_plan_configuration_id` FOREIGN KEY (`configuration_id`) REFERENCES `plans` (`id`) +); + +CREATE TABLE plan_configuration +( + `id` int NOT NULL AUTO_INCREMENT, + `title` varchar(255) DEFAULT NULL, + `description` varchar(255) DEFAULT NULL, + `quantity` int DEFAULT NULL, + `price` DECIMAL(10,2) DEFAULT NULL, + `currency` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`) +); + +CREATE TABLE payment_status ( `id` int NOT NULL AUTO_INCREMENT, `description` varchar(255) DEFAULT NULL, @@ -79,3 +99,21 @@ CREATE TABLE user_preferences CONSTRAINT `FK_user_preferences_diet_id` FOREIGN KEY (`diet_id`) REFERENCES `diets` (`id`), CONSTRAINT `FK_user_preferences_cook_level_id` FOREIGN KEY (`cook_level_id`) REFERENCES `cook_levels` (`id`) ); + +CREATE TABLE user_payments +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL, + `to_plan_id` int DEFAULT NULL, + `status_id` int DEFAULT NULL, + `external_id` varchar(255) DEFAULT NULL, + `external_reference` varchar(255) DEFAULT NULL, + `checkout_url` varchar(255) DEFAULT NULL, + `created_at` datetime(6) DEFAULT NULL, + `updated_at` datetime(6) DEFAULT NULL, + `deleted_at` datetime(6) DEFAULT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `FK_user_payments_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`), + CONSTRAINT `FK_user_payments_plan_id` FOREIGN KEY (`to_plan_id`) REFERENCES `plans` (`id`), + CONSTRAINT `FK_user_payments_status_id` FOREIGN KEY (`status_id`) REFERENCES `payment_status` (`id`) +); diff --git a/src/main/resources/sql/ddl/05_inserts.sql b/src/main/resources/sql/ddl/05_inserts.sql index 38682af..46c4322 100644 --- a/src/main/resources/sql/ddl/05_inserts.sql +++ b/src/main/resources/sql/ddl/05_inserts.sql @@ -2,6 +2,18 @@ INSERT INTO plans (id, description) VALUES (1, 'Free'), (2, 'Pro'); +INSERT INTO plan_configuration (id, title, description, quantity, price, currency) +VALUES (1, 'Cuoco Pro - Plan Premium', 'Actualiza a Pro: Recetas ilimitadas, filtros avanzados, meal preps y mucho más', 1, 500.00, 'ARS'); + +INSERT INTO payment_status (id, description) +VALUES (1, 'pending'), + (2, 'approved'), + (3, 'in_process'), + (4, 'rejected'), + (5, 'cancelled'), + (6, 'refunded'), + (7, 'charged_back'); + INSERT INTO cook_levels (id, description) VALUES (1, 'Bajo'), (2, 'Medio'),