diff --git a/src/main/java/com/zufar/onlinestore/cart/api/ItemsTotalPriceCalculator.java b/src/main/java/com/zufar/onlinestore/cart/api/ItemsTotalPriceCalculator.java index 957c0629..b5b661ca 100644 --- a/src/main/java/com/zufar/onlinestore/cart/api/ItemsTotalPriceCalculator.java +++ b/src/main/java/com/zufar/onlinestore/cart/api/ItemsTotalPriceCalculator.java @@ -1,17 +1,18 @@ package com.zufar.onlinestore.cart.api; -import com.zufar.onlinestore.cart.dto.ShoppingSessionItemDto; +import com.zufar.onlinestore.cart.entity.ShoppingSessionItem; +import org.mapstruct.Named; import org.springframework.stereotype.Service; import java.math.BigDecimal; -import java.util.List; +import java.util.Set; @Service public class ItemsTotalPriceCalculator { - public BigDecimal calculate(List items) { + @Named("toItemsTotalPrice") + public BigDecimal calculate(Set items) { return items.stream() - .map(item -> item.productInfo().price() - .multiply(BigDecimal.valueOf(item.productsQuantity()))) + .map(item -> item.getProductInfo().getPrice().multiply(BigDecimal.valueOf(item.getProductsQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); } } diff --git a/src/main/java/com/zufar/onlinestore/cart/converter/ShoppingSessionDtoConverter.java b/src/main/java/com/zufar/onlinestore/cart/converter/ShoppingSessionDtoConverter.java index b8431b1e..6e67acf0 100644 --- a/src/main/java/com/zufar/onlinestore/cart/converter/ShoppingSessionDtoConverter.java +++ b/src/main/java/com/zufar/onlinestore/cart/converter/ShoppingSessionDtoConverter.java @@ -1,14 +1,16 @@ package com.zufar.onlinestore.cart.converter; +import com.zufar.onlinestore.cart.api.ItemsTotalPriceCalculator; import com.zufar.onlinestore.cart.dto.ShoppingSessionDto; import com.zufar.onlinestore.cart.entity.ShoppingSession; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; - -@Mapper(componentModel = "spring", uses = ShoppingSessionItemDtoConverter.class) +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, uses = {ShoppingSessionItemDtoConverter.class, ItemsTotalPriceCalculator.class}) public interface ShoppingSessionDtoConverter { @Mapping(target = "items", source = "entity.items", qualifiedByName = {"toShoppingSessionItemDto"}) + @Mapping(target = "itemsTotalPrice", source = "entity.items", qualifiedByName = {"toItemsTotalPrice"}) ShoppingSessionDto toDto(final ShoppingSession entity); -} \ No newline at end of file +} diff --git a/src/main/java/com/zufar/onlinestore/cart/converter/ShoppingSessionItemDtoConverter.java b/src/main/java/com/zufar/onlinestore/cart/converter/ShoppingSessionItemDtoConverter.java index 90b046d3..d77288c5 100644 --- a/src/main/java/com/zufar/onlinestore/cart/converter/ShoppingSessionItemDtoConverter.java +++ b/src/main/java/com/zufar/onlinestore/cart/converter/ShoppingSessionItemDtoConverter.java @@ -5,13 +5,14 @@ import com.zufar.onlinestore.product.converter.ProductInfoDtoConverter; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; import org.mapstruct.Named; -@Mapper(componentModel = "spring", uses = ProductInfoDtoConverter.class) +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, uses = ProductInfoDtoConverter.class) public interface ShoppingSessionItemDtoConverter { @Named("toShoppingSessionItemDto") @Mapping(target = "productInfo", source = "entity.productInfo", qualifiedByName = {"toProductInfoFullDto"}) ShoppingSessionItemDto toDto(final ShoppingSessionItem entity); -} \ No newline at end of file +} diff --git a/src/main/java/com/zufar/onlinestore/payment/api/PaymentApi.java b/src/main/java/com/zufar/onlinestore/payment/api/PaymentApi.java index 6f5973dd..c4f375bf 100644 --- a/src/main/java/com/zufar/onlinestore/payment/api/PaymentApi.java +++ b/src/main/java/com/zufar/onlinestore/payment/api/PaymentApi.java @@ -1,52 +1,41 @@ package com.zufar.onlinestore.payment.api; -import com.zufar.onlinestore.payment.dto.CreatePaymentDto; -import com.zufar.onlinestore.payment.dto.CreatePaymentMethodDto; -import com.zufar.onlinestore.payment.dto.PaymentDetailsDto; -import com.zufar.onlinestore.payment.dto.PaymentDetailsWithTokenDto; -import com.zufar.onlinestore.payment.exception.PaymentEventParsingException; -import com.zufar.onlinestore.payment.exception.PaymentEventProcessingException; -import com.zufar.onlinestore.payment.exception.PaymentIntentProcessingException; -import com.zufar.onlinestore.payment.exception.PaymentMethodProcessingException; -import com.zufar.onlinestore.payment.exception.PaymentNotFoundException; +import com.zufar.onlinestore.payment.api.dto.CreateCardDetailsTokenRequest; +import com.zufar.onlinestore.payment.api.dto.ProcessedPaymentDetailsDto; +import com.zufar.onlinestore.payment.api.dto.ProcessedPaymentWithClientSecretDto; public interface PaymentApi { /** - * This method allows to create a payment object + * This method is responsible for payment processing * - * @param createPaymentDto the request dto to create a payment object - * @return PaymentDetailsWithTokenDto combines payment details and a payment token for payment processing on the front end side - * @throws PaymentIntentProcessingException this error occurs in cases where the data passed to process a payment intent is not valid. + * @param cardDetailsTokenId is stripe token collected based on information about the user's payment card + * @return PaymentDetailsWithTokenDto combines payment identifier and payment token for processing on front-end side * */ - PaymentDetailsWithTokenDto createPayment(final CreatePaymentDto createPaymentDto) throws PaymentIntentProcessingException; + ProcessedPaymentWithClientSecretDto processPayment(final String cardDetailsTokenId); /** * This method allows to create a payment method object * - * @param createPaymentMethodDto the request dto to create a payment method object - * @return String payment method identifier, for secure method transfer using the Stripe API - * @throws PaymentMethodProcessingException this error occurs in cases when the data transmitted to create a payment method is not valid. + * @param paymentId the payment identifier for retrieve payment details + * @return PaymentDetailsDto is payment details object * */ - String createPaymentMethod(final CreatePaymentMethodDto createPaymentMethodDto) throws PaymentMethodProcessingException; + ProcessedPaymentDetailsDto getPaymentDetails(final Long paymentId); /** * This method allows to create a payment method object * - * @param paymentId the payment identifier to search payment details - * @return PaymentDetailsDto these are payment details - * @throws PaymentNotFoundException this error is thrown in cases when the payment by the passed identifier was not found + * @param paymentIntentPayload string describing of the payment intent event type. + * @param stripeSignatureHeader stripe signature, which provide safe work with Stripe API webhooks mechanism * */ - PaymentDetailsDto getPaymentDetails(final Long paymentId) throws PaymentNotFoundException; + void processPaymentEvent(final String paymentIntentPayload, final String stripeSignatureHeader); /** - * This method allows to create a payment method object + * This method is a temporary solution until the front-end goes functional to pass us a token with payment card details. + * In the meantime, this method serves to test the payment api. * - * @param paymentIntentPayload this param it is a string describing of the payment intent event type. - * @param stripeSignatureHeader this param it is a string describing of the stripe signature, which provide safe work with Stripe API webhooks mechanism - * @throws PaymentEventProcessingException this error occurs in cases where webhook event data is not valid. - * @throws PaymentEventParsingException this error occurs when it is impossible to start the event in the intent due to invalid data + * @param createCardDetailsTokenRequest object that contains data about customer payment card. + * @return returns card details token in string form * */ - void processPaymentEvent(final String paymentIntentPayload, final String stripeSignatureHeader) throws PaymentEventProcessingException, PaymentEventParsingException; - + String processCardDetailsToken(final CreateCardDetailsTokenRequest createCardDetailsTokenRequest); } \ No newline at end of file diff --git a/src/main/java/com/zufar/onlinestore/payment/api/dto/CreateCardDetailsTokenRequest.java b/src/main/java/com/zufar/onlinestore/payment/api/dto/CreateCardDetailsTokenRequest.java new file mode 100644 index 00000000..f59f2e1a --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/api/dto/CreateCardDetailsTokenRequest.java @@ -0,0 +1,21 @@ +package com.zufar.onlinestore.payment.api.dto; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Builder; + +@Builder +public record CreateCardDetailsTokenRequest( + + @NotEmpty(message = "CardNumber is the mandatory attribute") + String cardNumber, + + @NotEmpty(message = "ExpMonth is the mandatory attribute") + String expMonth, + + @NotEmpty(message = "ExpYear is the mandatory attribute") + String expYear, + + @NotEmpty(message = "Cvc is the mandatory attribute") + String cvc +) { +} diff --git a/src/main/java/com/zufar/onlinestore/payment/api/dto/ProcessPaymentRequest.java b/src/main/java/com/zufar/onlinestore/payment/api/dto/ProcessPaymentRequest.java new file mode 100644 index 00000000..e015303f --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/api/dto/ProcessPaymentRequest.java @@ -0,0 +1,14 @@ +package com.zufar.onlinestore.payment.api.dto; + +import jakarta.validation.constraints.NotBlank; +import java.util.UUID; + +public record ProcessPaymentRequest( + + @NotBlank(message = "CardInfoToken is the mandatory attribute") + String cardInfoToken, + + @NotBlank(message = "CustomerId is the mandatory attribute") + UUID customerId +) { +} diff --git a/src/main/java/com/zufar/onlinestore/payment/dto/PaymentDetailsDto.java b/src/main/java/com/zufar/onlinestore/payment/api/dto/ProcessedPaymentDetailsDto.java similarity index 56% rename from src/main/java/com/zufar/onlinestore/payment/dto/PaymentDetailsDto.java rename to src/main/java/com/zufar/onlinestore/payment/api/dto/ProcessedPaymentDetailsDto.java index 8d9b546c..4535a3af 100644 --- a/src/main/java/com/zufar/onlinestore/payment/dto/PaymentDetailsDto.java +++ b/src/main/java/com/zufar/onlinestore/payment/api/dto/ProcessedPaymentDetailsDto.java @@ -1,14 +1,17 @@ -package com.zufar.onlinestore.payment.dto; +package com.zufar.onlinestore.payment.api.dto; +import com.zufar.onlinestore.cart.dto.ShoppingSessionItemDto; import com.zufar.onlinestore.payment.enums.PaymentStatus; import lombok.Builder; import java.math.BigDecimal; +import java.util.Set; @Builder -public record PaymentDetailsDto( +public record ProcessedPaymentDetailsDto( Long paymentId, - String paymentIntentId, BigDecimal itemsTotalPrice, + String paymentIntentId, + Set items, PaymentStatus status, String description ) { diff --git a/src/main/java/com/zufar/onlinestore/payment/api/dto/ProcessedPaymentWithClientSecretDto.java b/src/main/java/com/zufar/onlinestore/payment/api/dto/ProcessedPaymentWithClientSecretDto.java new file mode 100644 index 00000000..8aa855bf --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/api/dto/ProcessedPaymentWithClientSecretDto.java @@ -0,0 +1,10 @@ +package com.zufar.onlinestore.payment.api.dto; + +import lombok.Builder; + +@Builder +public record ProcessedPaymentWithClientSecretDto( + Long paymentId, + String clientSecret +) { +} diff --git a/src/main/java/com/zufar/onlinestore/payment/api/impl/PaymentApiImpl.java b/src/main/java/com/zufar/onlinestore/payment/api/impl/PaymentApiImpl.java index b116725c..c4ce715d 100644 --- a/src/main/java/com/zufar/onlinestore/payment/api/impl/PaymentApiImpl.java +++ b/src/main/java/com/zufar/onlinestore/payment/api/impl/PaymentApiImpl.java @@ -1,19 +1,13 @@ package com.zufar.onlinestore.payment.api.impl; import com.zufar.onlinestore.payment.api.PaymentApi; +import com.zufar.onlinestore.payment.api.dto.CreateCardDetailsTokenRequest; +import com.zufar.onlinestore.payment.api.dto.ProcessedPaymentDetailsDto; +import com.zufar.onlinestore.payment.api.dto.ProcessedPaymentWithClientSecretDto; import com.zufar.onlinestore.payment.api.impl.event.PaymentEventProcessor; -import com.zufar.onlinestore.payment.api.impl.intent.PaymentCreator; -import com.zufar.onlinestore.payment.api.impl.intent.PaymentMethodCreator; +import com.zufar.onlinestore.payment.api.impl.customer.CardDetailsProcessor; +import com.zufar.onlinestore.payment.api.impl.intent.PaymentProcessor; import com.zufar.onlinestore.payment.api.impl.intent.PaymentRetriever; -import com.zufar.onlinestore.payment.dto.CreatePaymentDto; -import com.zufar.onlinestore.payment.dto.CreatePaymentMethodDto; -import com.zufar.onlinestore.payment.dto.PaymentDetailsDto; -import com.zufar.onlinestore.payment.dto.PaymentDetailsWithTokenDto; -import com.zufar.onlinestore.payment.exception.PaymentEventParsingException; -import com.zufar.onlinestore.payment.exception.PaymentEventProcessingException; -import com.zufar.onlinestore.payment.exception.PaymentIntentProcessingException; -import com.zufar.onlinestore.payment.exception.PaymentMethodProcessingException; -import com.zufar.onlinestore.payment.exception.PaymentNotFoundException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -24,27 +18,27 @@ public class PaymentApiImpl implements PaymentApi { private final PaymentRetriever paymentRetriever; - private final PaymentCreator paymentCreator; - private final PaymentMethodCreator paymentMethodCreator; + private final PaymentProcessor paymentProcessor; private final PaymentEventProcessor paymentEventProcessor; + private final CardDetailsProcessor cardDetailsProcessor; @Override - public PaymentDetailsWithTokenDto createPayment(final CreatePaymentDto createPaymentDto) throws PaymentIntentProcessingException { - return paymentCreator.createPayment(createPaymentDto); + public ProcessedPaymentWithClientSecretDto processPayment(final String cardDetailsTokenId) { + return paymentProcessor.processPayment(cardDetailsTokenId); } @Override - public String createPaymentMethod(final CreatePaymentMethodDto createPaymentMethodDto) throws PaymentMethodProcessingException { - return paymentMethodCreator.createPaymentMethod(createPaymentMethodDto); + public ProcessedPaymentDetailsDto getPaymentDetails(final Long paymentId) { + return paymentRetriever.getPaymentDetails(paymentId); } @Override - public PaymentDetailsDto getPaymentDetails(Long paymentId) throws PaymentNotFoundException { - return paymentRetriever.getPaymentDetails(paymentId); + public void processPaymentEvent(final String paymentIntentPayload, final String stripeSignatureHeader) { + paymentEventProcessor.processPaymentEvent(paymentIntentPayload, stripeSignatureHeader); } @Override - public void processPaymentEvent(final String paymentIntentPayload, final String stripeSignatureHeader) throws PaymentEventProcessingException, PaymentEventParsingException { - paymentEventProcessor.processPaymentEvent(paymentIntentPayload, stripeSignatureHeader); + public String processCardDetailsToken(CreateCardDetailsTokenRequest createCardDetailsTokenRequest) { + return cardDetailsProcessor.processCardDetails(createCardDetailsTokenRequest); } } diff --git a/src/main/java/com/zufar/onlinestore/payment/api/impl/customer/CardDetailsProcessor.java b/src/main/java/com/zufar/onlinestore/payment/api/impl/customer/CardDetailsProcessor.java new file mode 100644 index 00000000..8c0f9fbd --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/api/impl/customer/CardDetailsProcessor.java @@ -0,0 +1,37 @@ +package com.zufar.onlinestore.payment.api.impl.customer; + +import com.stripe.exception.StripeException; +import com.stripe.model.Token; +import com.zufar.onlinestore.payment.api.dto.CreateCardDetailsTokenRequest; +import com.zufar.onlinestore.payment.config.StripeConfiguration; +import com.zufar.onlinestore.payment.exception.CardTokenCreationException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class CardDetailsProcessor { + + private final StripeConfiguration stripeConfiguration; + + public String processCardDetails(CreateCardDetailsTokenRequest createCardDetailsTokenRequest) { + StripeConfiguration.setStripeKey(stripeConfiguration.publishableKey()); + + Map card = createCardDetails(createCardDetailsTokenRequest); + Token cardDetailsToken; + try { + cardDetailsToken = Token.create(Map.of("card", card)); + } catch (StripeException e) { + throw new CardTokenCreationException(createCardDetailsTokenRequest.cardNumber()); + } + return cardDetailsToken.getId(); + } + + private Map createCardDetails(CreateCardDetailsTokenRequest createCardDetailsTokenRequest) { + return Map.of("number", createCardDetailsTokenRequest.cardNumber(), + "exp_month", Integer.parseInt(createCardDetailsTokenRequest.expMonth()), + "exp_year", Integer.parseInt(createCardDetailsTokenRequest.expYear()), + "cvc", createCardDetailsTokenRequest.cvc()); + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/api/impl/customer/StripeCustomerCreator.java b/src/main/java/com/zufar/onlinestore/payment/api/impl/customer/StripeCustomerCreator.java new file mode 100644 index 00000000..1bd15ce8 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/api/impl/customer/StripeCustomerCreator.java @@ -0,0 +1,35 @@ +package com.zufar.onlinestore.payment.api.impl.customer; + +import com.stripe.exception.StripeException; +import com.stripe.model.Customer; +import com.stripe.param.CustomerCreateParams; +import com.zufar.onlinestore.payment.converter.StripeCustomerConverter; +import com.zufar.onlinestore.payment.exception.StripeCustomerProcessingException; +import com.zufar.onlinestore.user.entity.UserEntity; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@RequiredArgsConstructor +@Service +public class StripeCustomerCreator { + + private final StripeCustomerConverter stripeCustomerConverter; + + public Customer createStripeCustomer(UserEntity authorizedUser, String paymentMethodToken) { + try { + log.info("Create stripe customer: in progress: start stripe customer creation"); + CustomerCreateParams customerCreateParams = stripeCustomerConverter.toStripeObject(authorizedUser, paymentMethodToken); + Customer createdStripeCustomer = Customer.create(customerCreateParams); + String createdStripeCustomerId = createdStripeCustomer.getId(); + log.info("Create stripe customer: successful: stripe customer was created with createdStripeCustomerId = {}.", createdStripeCustomerId); + + return createdStripeCustomer; + + } catch (StripeException e) { + log.error("Process stripe customer: failed: stripe customer was not created"); + throw new StripeCustomerProcessingException(authorizedUser.getEmail()); + } + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/api/impl/customer/StripeCustomerDataProcessor.java b/src/main/java/com/zufar/onlinestore/payment/api/impl/customer/StripeCustomerDataProcessor.java new file mode 100644 index 00000000..3b33ec5c --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/api/impl/customer/StripeCustomerDataProcessor.java @@ -0,0 +1,93 @@ +package com.zufar.onlinestore.payment.api.impl.customer; + +import com.stripe.exception.StripeException; +import com.stripe.model.Customer; +import com.stripe.model.PaymentMethod; +import com.zufar.onlinestore.payment.exception.CustomerRetrievingException; +import com.zufar.onlinestore.payment.exception.PaymentMethodNotFoundException; +import com.zufar.onlinestore.payment.exception.PaymentMethodRetrievingException; +import com.zufar.onlinestore.security.api.SecurityPrincipalProvider; +import com.zufar.onlinestore.user.entity.UserEntity; +import com.zufar.onlinestore.user.exception.UserNotFoundException; +import com.zufar.onlinestore.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.stereotype.Service; +import java.util.UUID; + +/** + * This class responsible for Stipe customer creation. It needed in order to connect + * customer with payment intent, this will ensure the correct management of payment by Stripe API + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class StripeCustomerDataProcessor { + + private final StripePaymentMethodProcessor stripePaymentMethodProcessor; + private final StripeCustomerCreator stripeCustomerCreator; + private final SecurityPrincipalProvider securityPrincipalProvider; + private final UserRepository userRepository; + + public Pair processStripeCustomerData(final String cardDetailsTokenId) { + log.info("Process stripe customer data: starting: start customer data processing"); + UUID authorizedUserId = securityPrincipalProvider.getUserId(); + UserEntity authorizedUser = userRepository.findById(authorizedUserId) + .orElseThrow(() -> new UserNotFoundException(authorizedUserId)); + + String stripeCustomerToken = authorizedUser.getStripeCustomerToken(); + + if (stripeCustomerToken != null) { + Customer retrievedStripeCustomer = retrieveStripeCustomer(stripeCustomerToken); + PaymentMethod retrievedStripePaymentMethod = retrieveStripePaymentMethodByStripeCustomer(retrievedStripeCustomer); + log.info("Process stripe customer data: finished: customer data was processed"); + + return Pair.of(authorizedUserId, retrievedStripePaymentMethod); + + } else { + PaymentMethod createdStripePaymentMethod = stripePaymentMethodProcessor.processStripePaymentMethod(cardDetailsTokenId); + Customer createdStripeCustomer = stripeCustomerCreator.createStripeCustomer(authorizedUser, createdStripePaymentMethod.getId()); + createdStripePaymentMethod.setCustomer(createdStripeCustomer.getId()); + + authorizedUser.setStripeCustomerToken(createdStripeCustomer.getId()); + userRepository.save(authorizedUser); + log.info("Process stripe customer data: finished: customer data was processed"); + + return Pair.of(authorizedUserId, createdStripePaymentMethod); + } + } + + private PaymentMethod retrieveStripePaymentMethodByStripeCustomer(Customer retrievedStripeCustomer) { + try { + log.info("Retrieve stripe payment method by stripe customer: in progress: start stripe payment retrieving."); + + PaymentMethod paymentMethod = retrievedStripeCustomer.listPaymentMethods() + .getData().stream() + .findFirst() + .orElseThrow(() -> new PaymentMethodNotFoundException(retrievedStripeCustomer.getId())); + + log.info("Retrieve stripe payment method by stripe customer: successfully: stripe payment method was retrieved."); + + return paymentMethod; + + } catch (StripeException e) { + log.error("Retrieve stripe payment method by stripe customer: failed: stripe payment method was not retrieved"); + throw new PaymentMethodRetrievingException(retrievedStripeCustomer.getId()); + } + } + + private Customer retrieveStripeCustomer(String stripeCustomerToken) { + try { + log.info("Retrieve stripe customer: in progress: start stripe customer retrieving by stripeCustomerToken = {}.", stripeCustomerToken); + Customer retrievedStripeCustomer = Customer.retrieve(stripeCustomerToken); + log.info("Retrieve stripe customer: successfully: stripe customer by stripeCustomerToken = {} was retrieved.", stripeCustomerToken); + + return retrievedStripeCustomer; + + } catch (StripeException e) { + log.error("Retrieve stripe customer: failed: stripe customer was not retrieved"); + throw new CustomerRetrievingException(stripeCustomerToken); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/zufar/onlinestore/payment/api/impl/customer/StripePaymentMethodProcessor.java b/src/main/java/com/zufar/onlinestore/payment/api/impl/customer/StripePaymentMethodProcessor.java new file mode 100644 index 00000000..6042576c --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/api/impl/customer/StripePaymentMethodProcessor.java @@ -0,0 +1,64 @@ +package com.zufar.onlinestore.payment.api.impl.customer; + +import com.stripe.exception.StripeException; +import com.stripe.model.PaymentMethod; +import com.stripe.model.Token; +import com.stripe.param.PaymentMethodCreateParams; +import com.zufar.onlinestore.payment.converter.StripePaymentMethodConverter; +import com.zufar.onlinestore.payment.exception.CardTokenRetrievingException; +import com.zufar.onlinestore.payment.exception.PaymentMethodProcessingException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * This class is responsible for converting passed parameters and creating based + * on their payment method (stripe object). Payment method is secondary object for + * creating and processing payment by Stripe API. + */ + +@Slf4j +@RequiredArgsConstructor +@Service +public class StripePaymentMethodProcessor { + + private final StripePaymentMethodConverter stripePaymentMethodConverter; + + public PaymentMethod processStripePaymentMethod(final String cardDetailsTokenId) { + Token retrievedToken = retrieveCardToken(cardDetailsTokenId); + + return createStripePaymentMethod(retrievedToken); + } + + private PaymentMethod createStripePaymentMethod(Token retrievedToken) { + String paymentMethodType = ""; + try { + log.info("Create stripe payment method: in progress: start creation stripe payment method"); + PaymentMethodCreateParams paymentMethodCreateParams = stripePaymentMethodConverter.toStripeObject(retrievedToken); + paymentMethodType = paymentMethodCreateParams.getType().getValue(); + PaymentMethod paymentMethod = PaymentMethod.create(paymentMethodCreateParams); + String paymentMethodId = paymentMethod.getId(); + log.info("Create stripe payment method: successfully: stripe payment method was created with paymentMethodId = {}.", paymentMethodId); + + return paymentMethod; + + } catch (StripeException ex) { + log.info("Create stripe payment method: failed: stripe payment method was not created."); + throw new PaymentMethodProcessingException(paymentMethodType); + } + } + + private Token retrieveCardToken(String cardDetailsTokenId) { + try { + log.info("Retrieve card token: in progress: start to retrieve card token by cardDetailsTokenId = {}.", cardDetailsTokenId); + Token retrievedCardToken = Token.retrieve(cardDetailsTokenId); + log.info("Retrieve card token: successfully: card token was retrieved with id = {}.", retrievedCardToken.getId()); + + return retrievedCardToken; + + } catch (StripeException e) { + log.error("Retrieve card token: failed: card token cannot retrieve"); + throw new CardTokenRetrievingException(cardDetailsTokenId); + } + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/api/impl/event/PaymentEventCreator.java b/src/main/java/com/zufar/onlinestore/payment/api/impl/event/PaymentEventCreator.java index 109ceae1..82d8f69a 100644 --- a/src/main/java/com/zufar/onlinestore/payment/api/impl/event/PaymentEventCreator.java +++ b/src/main/java/com/zufar/onlinestore/payment/api/impl/event/PaymentEventCreator.java @@ -20,7 +20,7 @@ public class PaymentEventCreator { private final StripeConfiguration stripeConfig; - public Event createPaymentEvent(String paymentIntentPayload, String stripeSignatureHeader) throws PaymentEventProcessingException { + public Event createPaymentEvent(String paymentIntentPayload, String stripeSignatureHeader) { log.info("Create payment event: start payment event creation:" + " paymentIntentPayload: {}, stripeSignatureHeader: {}.", paymentIntentPayload, stripeSignatureHeader); try { diff --git a/src/main/java/com/zufar/onlinestore/payment/api/impl/event/PaymentEventProcessor.java b/src/main/java/com/zufar/onlinestore/payment/api/impl/event/PaymentEventProcessor.java index 79961282..0662e83f 100644 --- a/src/main/java/com/zufar/onlinestore/payment/api/impl/event/PaymentEventProcessor.java +++ b/src/main/java/com/zufar/onlinestore/payment/api/impl/event/PaymentEventProcessor.java @@ -23,7 +23,7 @@ public class PaymentEventProcessor { private final PaymentEventParser paymentEventParser; private final PaymentEventHandler paymentEventHandler; - public void processPaymentEvent(String paymentIntentPayload, String stripeSignatureHeader) throws PaymentEventProcessingException, PaymentEventParsingException { + public void processPaymentEvent(String paymentIntentPayload, String stripeSignatureHeader) { log.info("Process payment event: start payment event processing: input params paymentIntentPayload: {}," + "stripeSignatureHeader: {}.", paymentIntentPayload, stripeSignatureHeader); Event event = paymentEventCreator.createPaymentEvent(paymentIntentPayload, stripeSignatureHeader); diff --git a/src/main/java/com/zufar/onlinestore/payment/api/impl/intent/PaymentCreator.java b/src/main/java/com/zufar/onlinestore/payment/api/impl/intent/PaymentCreator.java index 9e8c48f6..0ab08e81 100644 --- a/src/main/java/com/zufar/onlinestore/payment/api/impl/intent/PaymentCreator.java +++ b/src/main/java/com/zufar/onlinestore/payment/api/impl/intent/PaymentCreator.java @@ -1,21 +1,25 @@ package com.zufar.onlinestore.payment.api.impl.intent; -import com.stripe.exception.StripeException; import com.stripe.model.PaymentIntent; -import com.zufar.onlinestore.payment.converter.PaymentConverter; -import com.zufar.onlinestore.payment.converter.PaymentIntentConverter; -import com.zufar.onlinestore.payment.dto.CreatePaymentDto; -import com.zufar.onlinestore.payment.dto.PaymentDetailsWithTokenDto; +import com.stripe.model.PaymentMethod; +import com.zufar.onlinestore.cart.api.CartApi; +import com.zufar.onlinestore.cart.dto.ShoppingSessionDto; +import com.zufar.onlinestore.payment.converter.StripePaymentIntentConverter; import com.zufar.onlinestore.payment.entity.Payment; -import com.zufar.onlinestore.payment.exception.PaymentIntentProcessingException; +import com.zufar.onlinestore.payment.enums.PaymentStatus; +import com.zufar.onlinestore.payment.exception.ShoppingSessionAlreadyPaidException; import com.zufar.onlinestore.payment.repository.PaymentRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import java.util.UUID; + /** * This class is responsible for filling payment entity based on payment intent stripe object, * saving payment entity in database and for transferring payment token, which using on the front-end @@ -27,24 +31,38 @@ @Service public class PaymentCreator { + private final CartApi cartApi; private final PaymentRepository paymentRepository; - private final PaymentIntentCreator paymentIntentCreator; - private final PaymentIntentConverter paymentIntentConverter; - private final PaymentConverter paymentConverter; + private final StripePaymentIntentConverter stripePaymentIntentConverter; + private final StripePaymentIntentCreator stripePaymentIntentCreator; @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED) - public PaymentDetailsWithTokenDto createPayment(final CreatePaymentDto createPaymentDto) throws PaymentIntentProcessingException { - PaymentIntent paymentIntent = paymentIntentCreator.createPaymentIntent(createPaymentDto); - log.info("Create payment: payment intent: {} successfully created.", paymentIntent); - String paymentToken = paymentIntent.getClientSecret(); - Payment payment = paymentIntentConverter.toPayment(paymentIntent); - Payment savedPayment = paymentRepository.save(payment); - log.info("Create payment: payment {} successfully saved.", savedPayment); - - return PaymentDetailsWithTokenDto.builder() - .paymentToken(paymentToken) - .paymentDetailsDto((paymentConverter.toDto(savedPayment))) - .build(); + public Pair createPayment(final Pair pair) { + log.info("Create payment: starting: start payment creation"); + UUID userId = pair.getLeft(); + PaymentMethod paymentMethod = pair.getRight(); + + ShoppingSessionDto shoppingSession = cartApi.getShoppingSessionByUserId(userId); + PaymentIntent stripePaymentIntent = stripePaymentIntentCreator.createStripePaymentIntent(paymentMethod, shoppingSession); + Payment paymentToSave = fillPaymentDetails(shoppingSession, stripePaymentIntent); + try { + Payment savedPayment = paymentRepository.save(paymentToSave); + log.info("Create payment: finishing: payment was created"); + + return Pair.of(stripePaymentIntent.getClientSecret(), savedPayment); + + } catch (DataIntegrityViolationException e) { + throw new ShoppingSessionAlreadyPaidException(shoppingSession.id()); + } } + private Payment fillPaymentDetails(ShoppingSessionDto shoppingSession, PaymentIntent stripePaymentIntent) { + log.info("Fill payment details: starting: start payment object filling"); + Payment payment = stripePaymentIntentConverter.toEntity(stripePaymentIntent, shoppingSession); + payment.setStatus(PaymentStatus.PAYMENT_IS_PROCESSING); + payment.setDescription(PaymentStatus.PAYMENT_IS_PROCESSING.getDescription()); + log.info("Fill payment details: finished: payment object was filled"); + + return payment; + } } diff --git a/src/main/java/com/zufar/onlinestore/payment/api/impl/intent/PaymentIntentCreator.java b/src/main/java/com/zufar/onlinestore/payment/api/impl/intent/PaymentIntentCreator.java deleted file mode 100644 index b16334f2..00000000 --- a/src/main/java/com/zufar/onlinestore/payment/api/impl/intent/PaymentIntentCreator.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.zufar.onlinestore.payment.api.impl.intent; - -import com.stripe.Stripe; -import com.stripe.exception.StripeException; -import com.stripe.model.PaymentIntent; -import com.stripe.param.PaymentIntentCreateParams; -import com.zufar.onlinestore.payment.config.StripeConfiguration; -import com.zufar.onlinestore.payment.converter.PaymentIntentConverter; -import com.zufar.onlinestore.payment.dto.CreatePaymentDto; -import com.zufar.onlinestore.payment.exception.PaymentIntentProcessingException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -/** - * This class is responsible for converting passed parameters and creating based - * on their payment intent (stripe object). Payment intent is the main object for - * creating and processing payment by Stripe API. - * */ - -@Slf4j -@RequiredArgsConstructor -@Service -public class PaymentIntentCreator { - - private final StripeConfiguration stripeConfig; - private final PaymentIntentConverter paymentIntentConverter; - - public PaymentIntent createPaymentIntent(final CreatePaymentDto createPaymentDto) throws PaymentIntentProcessingException { - String stripeKey = stripeConfig.secretKey(); - StripeConfiguration.setStripeKey(stripeKey); - - PaymentIntentCreateParams params = paymentIntentConverter.toPaymentIntentParams(createPaymentDto); - log.info("Create payment intent: payment intent params: {} for creation.", params); - - try { - return PaymentIntent.create(params); - } catch (StripeException ex) { - log.error("Error during Payment processing", ex); - throw new PaymentIntentProcessingException(params.getPaymentMethod()); - } - } - -} diff --git a/src/main/java/com/zufar/onlinestore/payment/api/impl/intent/PaymentMethodCreator.java b/src/main/java/com/zufar/onlinestore/payment/api/impl/intent/PaymentMethodCreator.java deleted file mode 100644 index 37714b76..00000000 --- a/src/main/java/com/zufar/onlinestore/payment/api/impl/intent/PaymentMethodCreator.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.zufar.onlinestore.payment.api.impl.intent; - -import com.stripe.Stripe; -import com.stripe.exception.StripeException; -import com.stripe.model.PaymentMethod; -import com.stripe.param.PaymentMethodCreateParams; -import com.zufar.onlinestore.payment.config.StripeConfiguration; -import com.zufar.onlinestore.payment.converter.PaymentMethodConverter; -import com.zufar.onlinestore.payment.dto.CreatePaymentMethodDto; -import com.zufar.onlinestore.payment.exception.PaymentMethodProcessingException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -/** - * This class is responsible for converting passed parameters and creating based - * on their payment method (stripe object). Payment method is secondary object for - * creating and processing payment by Stripe API. - * */ - -@Slf4j -@RequiredArgsConstructor -@Service -public class PaymentMethodCreator { - - private final StripeConfiguration stripeConfig; - private final PaymentMethodConverter paymentMethodConverter; - - public String createPaymentMethod(final CreatePaymentMethodDto createPaymentMethodDto) throws PaymentMethodProcessingException{ - String stripeKey = stripeConfig.publishableKey(); - StripeConfiguration.setStripeKey(stripeKey); - - PaymentMethod paymentMethod; - PaymentMethodCreateParams params = paymentMethodConverter.toPaymentMethodParams(createPaymentMethodDto); - try { - paymentMethod = PaymentMethod.create(params); - } catch (StripeException ex) { - log.error("Error during payment method processing", ex); - throw new PaymentMethodProcessingException(params.getType().getValue()); - } - log.info("Created payment method: payment method has been created: paymentMethod: {}.", paymentMethod); - - return paymentMethod.getId(); - } -} diff --git a/src/main/java/com/zufar/onlinestore/payment/api/impl/intent/PaymentProcessor.java b/src/main/java/com/zufar/onlinestore/payment/api/impl/intent/PaymentProcessor.java new file mode 100644 index 00000000..0d500db5 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/api/impl/intent/PaymentProcessor.java @@ -0,0 +1,40 @@ +package com.zufar.onlinestore.payment.api.impl.intent; + +import com.stripe.model.PaymentMethod; +import com.zufar.onlinestore.payment.api.dto.ProcessedPaymentWithClientSecretDto; +import com.zufar.onlinestore.payment.api.impl.customer.StripeCustomerDataProcessor; +import com.zufar.onlinestore.payment.config.StripeConfiguration; +import com.zufar.onlinestore.payment.entity.Payment; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.stereotype.Service; +import java.util.UUID; + +@Slf4j +@RequiredArgsConstructor +@Service +public class PaymentProcessor { + + private final StripeCustomerDataProcessor stripeCustomerDataProcessor; + private final StripeConfiguration stripeConfiguration; + private final PaymentCreator paymentCreator; + + public ProcessedPaymentWithClientSecretDto processPayment(final String cardDetailsTokenId) { + log.info("Process payment: starting: processing payment with cardDetailsTokenId = {}.", cardDetailsTokenId); + StripeConfiguration.setStripeKey(stripeConfiguration.secretKey()); + + Pair userIdAndPaymentMethodPair = stripeCustomerDataProcessor.processStripeCustomerData(cardDetailsTokenId); + Pair clientSecretAndPaymentPair = paymentCreator.createPayment(userIdAndPaymentMethodPair); + String clientSecret = clientSecretAndPaymentPair.getLeft(); + Payment payment = clientSecretAndPaymentPair.getRight(); + + Long paymentId = payment.getPaymentId(); + log.info("Process payment: finishing: payment was processed with paymentId = {}.", paymentId); + + return ProcessedPaymentWithClientSecretDto.builder() + .paymentId(paymentId) + .clientSecret(clientSecret) + .build(); + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/api/impl/intent/PaymentRetriever.java b/src/main/java/com/zufar/onlinestore/payment/api/impl/intent/PaymentRetriever.java index 7c08dc0c..44a719c1 100644 --- a/src/main/java/com/zufar/onlinestore/payment/api/impl/intent/PaymentRetriever.java +++ b/src/main/java/com/zufar/onlinestore/payment/api/impl/intent/PaymentRetriever.java @@ -1,7 +1,9 @@ package com.zufar.onlinestore.payment.api.impl.intent; +import com.zufar.onlinestore.cart.exception.ShoppingSessionNotFoundException; +import com.zufar.onlinestore.cart.repository.ShoppingSessionRepository; import com.zufar.onlinestore.payment.converter.PaymentConverter; -import com.zufar.onlinestore.payment.dto.PaymentDetailsDto; +import com.zufar.onlinestore.payment.api.dto.ProcessedPaymentDetailsDto; import com.zufar.onlinestore.payment.exception.PaymentNotFoundException; import com.zufar.onlinestore.payment.repository.PaymentRepository; import lombok.RequiredArgsConstructor; @@ -10,8 +12,8 @@ import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; - import java.util.Objects; +import java.util.UUID; /** * This class is responsible for retrieving relevant payment details from database @@ -22,16 +24,22 @@ @Service public class PaymentRetriever { + private final ShoppingSessionRepository shoppingSessionRepository; private final PaymentRepository paymentRepository; private final PaymentConverter paymentConverter; - @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, readOnly = true) - public PaymentDetailsDto getPaymentDetails(Long paymentId) throws PaymentNotFoundException { + @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED) + public ProcessedPaymentDetailsDto getPaymentDetails(final Long paymentId) { Objects.requireNonNull(paymentId); - log.info("Get payment details: start payment details retrieve by payment id: {}.", paymentId); + log.info("Get payment details: starting: payment details retrieve by payment id = {}.", paymentId); return paymentRepository.findById(paymentId) - .map(paymentConverter::toDto) + .map(payment -> { + UUID shoppingSessionId = payment.getShoppingSessionId(); + return shoppingSessionRepository.findById(shoppingSessionId) + .map(session -> paymentConverter.toDto(payment, session.getItems())) + .orElseThrow(() -> new ShoppingSessionNotFoundException(shoppingSessionId)); + }) .orElseThrow(() -> new PaymentNotFoundException(paymentId)); } } diff --git a/src/main/java/com/zufar/onlinestore/payment/api/impl/intent/StripePaymentIntentCreator.java b/src/main/java/com/zufar/onlinestore/payment/api/impl/intent/StripePaymentIntentCreator.java new file mode 100644 index 00000000..ab621180 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/api/impl/intent/StripePaymentIntentCreator.java @@ -0,0 +1,44 @@ +package com.zufar.onlinestore.payment.api.impl.intent; + +import com.stripe.exception.StripeException; +import com.stripe.model.PaymentIntent; +import com.stripe.model.PaymentMethod; +import com.stripe.param.PaymentIntentCreateParams; +import com.zufar.onlinestore.cart.dto.ShoppingSessionDto; +import com.zufar.onlinestore.payment.config.StripeConfiguration; +import com.zufar.onlinestore.payment.converter.StripePaymentIntentConverter; +import com.zufar.onlinestore.payment.exception.PaymentIntentProcessingException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * This class is responsible for converting passed parameters and creating based + * on their payment intent (stripe object). Payment intent is the main object for + * creating and processing payment by Stripe API. + * */ + +@Slf4j +@RequiredArgsConstructor +@Service +public class StripePaymentIntentCreator { + + private final StripePaymentIntentConverter stripePaymentIntentConverter; + private final StripeConfiguration stripeConfiguration; + + public PaymentIntent createStripePaymentIntent(final PaymentMethod paymentMethod, ShoppingSessionDto shoppingSession) { + log.info("Create stripe payment intent: starting: start payment intent creation"); + String currency = stripeConfiguration.currency(); + PaymentIntentCreateParams paymentIntentCreateParams = stripePaymentIntentConverter.toStripeObject(paymentMethod, shoppingSession, currency); + String paymentMethodId = paymentIntentCreateParams.getPaymentMethod(); + log.info("Create stripe payment intent: in progress: creation stripe payment intent with paymentMethodId = {}", paymentMethodId); + try { + PaymentIntent paymentIntent = PaymentIntent.create(paymentIntentCreateParams); + log.info("Create stripe payment intent: successful: stripe payment intent was created with id = {}", paymentIntent.getId()); + return paymentIntent; + } catch (StripeException ex) { + log.error("Create stripe payment intent: failed: stripe payment intent was not created"); + throw new PaymentIntentProcessingException(paymentMethodId); + } + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/calculator/PaymentPriceCalculator.java b/src/main/java/com/zufar/onlinestore/payment/calculator/PaymentPriceCalculator.java index 7eb0a211..ce678d31 100644 --- a/src/main/java/com/zufar/onlinestore/payment/calculator/PaymentPriceCalculator.java +++ b/src/main/java/com/zufar/onlinestore/payment/calculator/PaymentPriceCalculator.java @@ -10,7 +10,7 @@ @Component public class PaymentPriceCalculator { - private static final Long COIN_TO_CURRENCY_CONVERSION_VALUE = 100L; + private static final Double COIN_TO_CURRENCY_CONVERSION_VALUE = 100.0; @Named("calculateForPayment") public BigDecimal calculatePriceForPayment(Long totalPrice) { @@ -19,6 +19,6 @@ public BigDecimal calculatePriceForPayment(Long totalPrice) { @Named("calculateForPaymentIntent") public Long calculatePriceForPaymentIntent(BigDecimal totalPrice) { - return totalPrice.longValue() * COIN_TO_CURRENCY_CONVERSION_VALUE; + return totalPrice.multiply(BigDecimal.valueOf(COIN_TO_CURRENCY_CONVERSION_VALUE)).longValue(); } } diff --git a/src/main/java/com/zufar/onlinestore/payment/config/StripeConfiguration.java b/src/main/java/com/zufar/onlinestore/payment/config/StripeConfiguration.java index 7a410127..d2a18ee8 100644 --- a/src/main/java/com/zufar/onlinestore/payment/config/StripeConfiguration.java +++ b/src/main/java/com/zufar/onlinestore/payment/config/StripeConfiguration.java @@ -7,17 +7,18 @@ /** * This record responsible for displaying the Stripe Api configuration in Spring as a bean. * Configuration stores special keys with which Stripe controls data security. - * @param secretKey used to security work with the Stripe Api from the backend side. * - * @param publishableKey used to security work with the Stripe Api from the frontend side. - * - * @param webHookSecretKey used to security work with the Stripe Api from the webhooks side. + * @param secretKey used for security work with the Stripe Api from the backend side. + * @param publishableKey used for security work with the Stripe Api from the frontend side. + * @param webHookSecretKey used for security work with the Stripe Api from the webhooks side. + * @param currency used for provide currency for Stripe Api. * */ @ConfigurationProperties(prefix = "stripe") public record StripeConfiguration(String secretKey, String publishableKey, - String webHookSecretKey) { + String webHookSecretKey, + String currency) { @PostConstruct private void init() { diff --git a/src/main/java/com/zufar/onlinestore/payment/converter/PaymentConverter.java b/src/main/java/com/zufar/onlinestore/payment/converter/PaymentConverter.java index 7bedb7a2..e63a0329 100644 --- a/src/main/java/com/zufar/onlinestore/payment/converter/PaymentConverter.java +++ b/src/main/java/com/zufar/onlinestore/payment/converter/PaymentConverter.java @@ -1,11 +1,20 @@ package com.zufar.onlinestore.payment.converter; -import com.zufar.onlinestore.payment.dto.PaymentDetailsDto; +import com.zufar.onlinestore.cart.converter.ShoppingSessionItemDtoConverter; +import com.zufar.onlinestore.cart.entity.ShoppingSessionItem; +import com.zufar.onlinestore.payment.api.dto.ProcessedPaymentDetailsDto; import com.zufar.onlinestore.payment.entity.Payment; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; -@Mapper +import java.util.Set; + +@Mapper(uses = ShoppingSessionItemDtoConverter.class , componentModel = MappingConstants.ComponentModel.SPRING) public interface PaymentConverter { - PaymentDetailsDto toDto(final Payment entity); + @Mapping(target = "items", source = "shoppingSessionItems", qualifiedByName = {"toShoppingSessionItemDto"}) + ProcessedPaymentDetailsDto toDto(final Payment payment, + final Set shoppingSessionItems); + } diff --git a/src/main/java/com/zufar/onlinestore/payment/converter/PaymentIntentConverter.java b/src/main/java/com/zufar/onlinestore/payment/converter/PaymentIntentConverter.java deleted file mode 100644 index 565197da..00000000 --- a/src/main/java/com/zufar/onlinestore/payment/converter/PaymentIntentConverter.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.zufar.onlinestore.payment.converter; - -import com.stripe.model.PaymentIntent; -import com.stripe.param.PaymentIntentCreateParams; -import com.zufar.onlinestore.payment.calculator.PaymentPriceCalculator; -import com.zufar.onlinestore.payment.dto.CreatePaymentDto; -import com.zufar.onlinestore.payment.entity.Payment; -import org.mapstruct.BeanMapping; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; - -@Mapper(uses = PaymentPriceCalculator.class) -public interface PaymentIntentConverter { - - @BeanMapping(ignoreByDefault = true) - @Mapping(target = "paymentIntentId", source = "paymentIntent.id") - @Mapping(target = "itemsTotalPrice", source = "paymentIntent.amount", qualifiedByName = {"calculateForPayment"}) - Payment toPayment(final PaymentIntent paymentIntent); - - @Mapping(target = "currency", constant = "usd") - @Mapping(target = "paymentMethod", source = "createPaymentDto.paymentMethodId") - @Mapping(target = "amount", - source = "createPaymentDto.itemsTotalPrice", - qualifiedByName = {"calculateForPaymentIntent"}) - PaymentIntentCreateParams toPaymentIntentParams(final CreatePaymentDto createPaymentDto); -} diff --git a/src/main/java/com/zufar/onlinestore/payment/converter/PaymentMethodConverter.java b/src/main/java/com/zufar/onlinestore/payment/converter/PaymentMethodConverter.java deleted file mode 100644 index d0ac6b24..00000000 --- a/src/main/java/com/zufar/onlinestore/payment/converter/PaymentMethodConverter.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.zufar.onlinestore.payment.converter; - -import com.stripe.param.PaymentMethodCreateParams; -import com.zufar.onlinestore.payment.dto.CreatePaymentMethodDto; -import org.springframework.stereotype.Component; - -@Component -public class PaymentMethodConverter { - - public PaymentMethodCreateParams toPaymentMethodParams(final CreatePaymentMethodDto createPaymentMethodDto) { - PaymentMethodCreateParams.Builder paymentMethod = PaymentMethodCreateParams.builder(); - PaymentMethodCreateParams.CardDetails cardDetails = getCardDetails(createPaymentMethodDto); - - paymentMethod.setCard(cardDetails); - paymentMethod.setType(PaymentMethodCreateParams.Type.CARD); - - return paymentMethod.build(); - } - - private PaymentMethodCreateParams.CardDetails getCardDetails(final CreatePaymentMethodDto createPaymentMethodDto) { - PaymentMethodCreateParams.CardDetails.Builder cardDetails = PaymentMethodCreateParams.CardDetails.builder(); - - cardDetails.setNumber(createPaymentMethodDto.cardNumber()); - cardDetails.setExpYear(createPaymentMethodDto.expYear()); - cardDetails.setExpMonth(createPaymentMethodDto.expMonth()); - cardDetails.setCvc(createPaymentMethodDto.cvc()); - - return cardDetails.build(); - } -} diff --git a/src/main/java/com/zufar/onlinestore/payment/converter/StripeCustomerConverter.java b/src/main/java/com/zufar/onlinestore/payment/converter/StripeCustomerConverter.java new file mode 100644 index 00000000..b2e9f4f4 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/converter/StripeCustomerConverter.java @@ -0,0 +1,43 @@ +package com.zufar.onlinestore.payment.converter; + +import com.stripe.param.CustomerCreateParams; +import com.zufar.onlinestore.user.entity.Address; +import com.zufar.onlinestore.user.entity.UserEntity; +import org.apache.commons.lang3.StringUtils; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.springframework.stereotype.Component; + +@Component +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface StripeCustomerConverter { + + @Mapping(target = "putAllExtraParam", ignore = true) + @Mapping(target = "cashBalance", ignore = true) + @Mapping(target = "invoiceSettings", ignore = true) + @Mapping(target = "putAllMetadata", ignore = true) + @Mapping(target = "tax", ignore = true) + @Mapping(target = "address", expression = "java(toAddress(authorizedUser.getAddress()))") + @Mapping(target = "name", expression = "java(toFullName(authorizedUser))") + @Mapping(target = "email", source = "authorizedUser.email") + @Mapping(target = "paymentMethod", source = "paymentMethodToken") + CustomerCreateParams toStripeObject(UserEntity authorizedUser, String paymentMethodToken); + + default CustomerCreateParams.Address toAddress(Address address) { + CustomerCreateParams.Address stripeAddress = null; + if (address != null) { + stripeAddress = CustomerCreateParams.Address.builder() + .setCountry(address.getCountry()) + .setCity(address.getCity()) + .setLine1(address.getLine()) + .build(); + } + return stripeAddress; + } + + default String toFullName(UserEntity authorizedUser) { + return StringUtils.join(authorizedUser.getFirstName(), Character.SPACE_SEPARATOR, authorizedUser.getLastName()); + } +} + diff --git a/src/main/java/com/zufar/onlinestore/payment/converter/StripePaymentIntentConverter.java b/src/main/java/com/zufar/onlinestore/payment/converter/StripePaymentIntentConverter.java new file mode 100644 index 00000000..d0ddbd9d --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/converter/StripePaymentIntentConverter.java @@ -0,0 +1,31 @@ +package com.zufar.onlinestore.payment.converter; + +import com.stripe.model.PaymentIntent; +import com.stripe.model.PaymentMethod; +import com.stripe.param.PaymentIntentCreateParams; +import com.zufar.onlinestore.cart.dto.ShoppingSessionDto; +import com.zufar.onlinestore.payment.calculator.PaymentPriceCalculator; +import com.zufar.onlinestore.payment.entity.Payment;; +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; + +@Mapper(uses = PaymentPriceCalculator.class, componentModel = MappingConstants.ComponentModel.SPRING) +public interface StripePaymentIntentConverter { + + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "paymentIntentId", source = "paymentIntent.id") + @Mapping(target = "shoppingSessionId", source = "shoppingSession.id") + @Mapping(target = "itemsTotalPrice", source = "paymentIntent.amount", qualifiedByName = {"calculateForPayment"}) + Payment toEntity(final PaymentIntent paymentIntent, + final ShoppingSessionDto shoppingSession); + + @Mapping(target = "customer", source = "paymentMethod.customer") + @Mapping(target = "paymentMethod", source = "paymentMethod.id") + @Mapping(target = "amount", + source = "shoppingSession.itemsTotalPrice", + qualifiedByName = {"calculateForPaymentIntent"}) + PaymentIntentCreateParams toStripeObject(final PaymentMethod paymentMethod, + final ShoppingSessionDto shoppingSession, final String currency); +} diff --git a/src/main/java/com/zufar/onlinestore/payment/converter/StripePaymentMethodConverter.java b/src/main/java/com/zufar/onlinestore/payment/converter/StripePaymentMethodConverter.java new file mode 100644 index 00000000..e1df7c06 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/converter/StripePaymentMethodConverter.java @@ -0,0 +1,28 @@ +package com.zufar.onlinestore.payment.converter; + +import com.stripe.model.Token; +import com.stripe.param.PaymentMethodCreateParams; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingConstants; +import org.mapstruct.Named; + +@Mapper(componentModel = MappingConstants.ComponentModel.SPRING) +public interface StripePaymentMethodConverter { + + @Mapping(target = "type", source = "token", qualifiedByName = {"toType"}) + @Mapping(target = "card", source = "token.id", qualifiedByName = {"toToken"}) + PaymentMethodCreateParams toStripeObject(Token token); + + @Named("toToken") + default PaymentMethodCreateParams.Token toToken(String tokenId) { + return PaymentMethodCreateParams.Token.builder() + .setToken(tokenId) + .build(); + } + + @Named("toType") + default PaymentMethodCreateParams.Type toType(Token token) { + return PaymentMethodCreateParams.Type.valueOf(token.getType().toUpperCase()); + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/dto/CreatePaymentDto.java b/src/main/java/com/zufar/onlinestore/payment/dto/CreatePaymentDto.java deleted file mode 100644 index baf8df34..00000000 --- a/src/main/java/com/zufar/onlinestore/payment/dto/CreatePaymentDto.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.zufar.onlinestore.payment.dto; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import java.math.BigDecimal; - -public record CreatePaymentDto( - - @NotBlank(message = "PaymentMethodId is the mandatory attribute") - String paymentMethodId, - - @NotNull(message = "ItemsTotalPrice is mandatory attribute") - BigDecimal itemsTotalPrice -) { -} diff --git a/src/main/java/com/zufar/onlinestore/payment/dto/CreatePaymentMethodDto.java b/src/main/java/com/zufar/onlinestore/payment/dto/CreatePaymentMethodDto.java deleted file mode 100644 index 71ba734b..00000000 --- a/src/main/java/com/zufar/onlinestore/payment/dto/CreatePaymentMethodDto.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.zufar.onlinestore.payment.dto; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -public record CreatePaymentMethodDto ( - - @NotBlank(message = "CardNumber is the mandatory attribute") - String cardNumber, - - @NotNull(message = "ExpMonth is the mandatory attribute") - Long expMonth, - - @NotNull(message = "ExpYear is the mandatory attribute") - Long expYear, - - @NotNull(message = "Cvc is the mandatory attribute") - String cvc -) { -} \ No newline at end of file diff --git a/src/main/java/com/zufar/onlinestore/payment/dto/PaymentDetailsWithTokenDto.java b/src/main/java/com/zufar/onlinestore/payment/dto/PaymentDetailsWithTokenDto.java deleted file mode 100644 index 7c267776..00000000 --- a/src/main/java/com/zufar/onlinestore/payment/dto/PaymentDetailsWithTokenDto.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.zufar.onlinestore.payment.dto; - -import lombok.Builder; - -@Builder -public record PaymentDetailsWithTokenDto( - String paymentToken, - PaymentDetailsDto paymentDetailsDto -) { -} diff --git a/src/main/java/com/zufar/onlinestore/payment/endpoint/PaymentEndpoint.java b/src/main/java/com/zufar/onlinestore/payment/endpoint/PaymentEndpoint.java index 4f0b01f6..247d1a50 100644 --- a/src/main/java/com/zufar/onlinestore/payment/endpoint/PaymentEndpoint.java +++ b/src/main/java/com/zufar/onlinestore/payment/endpoint/PaymentEndpoint.java @@ -1,15 +1,10 @@ package com.zufar.onlinestore.payment.endpoint; +import com.zufar.onlinestore.common.response.ApiResponse; import com.zufar.onlinestore.payment.api.PaymentApi; -import com.zufar.onlinestore.payment.dto.CreatePaymentDto; -import com.zufar.onlinestore.payment.dto.CreatePaymentMethodDto; -import com.zufar.onlinestore.payment.dto.PaymentDetailsDto; -import com.zufar.onlinestore.payment.dto.PaymentDetailsWithTokenDto; -import com.zufar.onlinestore.payment.exception.PaymentEventParsingException; -import com.zufar.onlinestore.payment.exception.PaymentEventProcessingException; -import com.zufar.onlinestore.payment.exception.PaymentIntentProcessingException; -import com.zufar.onlinestore.payment.exception.PaymentMethodProcessingException; -import com.zufar.onlinestore.payment.exception.PaymentNotFoundException; +import com.zufar.onlinestore.payment.api.dto.CreateCardDetailsTokenRequest; +import com.zufar.onlinestore.payment.api.dto.ProcessedPaymentDetailsDto; +import com.zufar.onlinestore.payment.api.dto.ProcessedPaymentWithClientSecretDto; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @@ -18,13 +13,15 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import java.time.LocalDateTime; @Slf4j @Validated @@ -38,36 +35,57 @@ public class PaymentEndpoint { private final PaymentApi paymentApi; @PostMapping - public ResponseEntity createPayment(@RequestBody @Valid final CreatePaymentDto createPaymentDto) throws PaymentIntentProcessingException { - PaymentDetailsWithTokenDto createdPayment = paymentApi.createPayment(createPaymentDto); + public ResponseEntity> processPayment(@RequestParam @NotEmpty final String cardDetailsTokenId) { + ProcessedPaymentWithClientSecretDto processedPayment = paymentApi.processPayment(cardDetailsTokenId); + + ApiResponse apiResponse = ApiResponse.builder() + .data(processedPayment) + .timestamp(LocalDateTime.now()) + .httpStatusCode(HttpStatus.CREATED.value()) + .build(); + return ResponseEntity.status(HttpStatus.CREATED) - .body(createdPayment); + .body(apiResponse); } @GetMapping("/{paymentId}") - public ResponseEntity getPaymentDetails(@PathVariable @NotNull final Long paymentId) throws PaymentNotFoundException { - PaymentDetailsDto retrievedPayment = paymentApi.getPaymentDetails(paymentId); - log.info("Get payment details: payment details: {} successfully retrieved.", retrievedPayment); - return ResponseEntity.ok() - .body(retrievedPayment); - } + public ResponseEntity> getPaymentDetails(@PathVariable @NotNull final Long paymentId) { + ProcessedPaymentDetailsDto retrievedPayment = paymentApi.getPaymentDetails(paymentId); - /** - * This endpoint is used only until we have an implementation of this logic on the frontend side. - * It will come in handy for testing the API. - */ - @PostMapping("/method") - public ResponseEntity getPaymentMethod(@RequestBody @Valid final CreatePaymentMethodDto createPaymentMethodDto) throws PaymentMethodProcessingException { - String paymentMethodId = paymentApi.createPaymentMethod(createPaymentMethodDto); - return ResponseEntity.ok() - .body(paymentMethodId); + ApiResponse apiResponse = ApiResponse.builder() + .data(retrievedPayment) + .timestamp(LocalDateTime.now()) + .httpStatusCode(HttpStatus.OK.value()) + .build(); + + return ResponseEntity.status(HttpStatus.OK) + .body(apiResponse); } @PostMapping("/event") - public ResponseEntity paymentEventsProcess(@RequestBody @NotEmpty final String paymentIntentPayload, - @RequestHeader("Stripe-Signature") @NotEmpty final String stripeSignatureHeader) throws PaymentEventProcessingException, PaymentEventParsingException { + public ResponseEntity> paymentEventProcess(@RequestBody @NotEmpty final String paymentIntentPayload, @RequestHeader("Stripe-Signature") @NotEmpty final String stripeSignatureHeader) { paymentApi.processPaymentEvent(paymentIntentPayload, stripeSignatureHeader); - return ResponseEntity.ok() + + ApiResponse apiResponse = ApiResponse.builder() + .timestamp(LocalDateTime.now()) + .httpStatusCode(HttpStatus.OK.value()) + .build(); + + return ResponseEntity.status(HttpStatus.OK) + .body(apiResponse); + } + + @PostMapping("/card") + public ResponseEntity> processCardDetailsToken(@RequestBody @Valid final CreateCardDetailsTokenRequest createCardDetailsTokenRequest) { + String cardDetailsTokenId = paymentApi.processCardDetailsToken(createCardDetailsTokenRequest); + + ApiResponse apiResponse = ApiResponse.builder() + .data(cardDetailsTokenId) + .timestamp(LocalDateTime.now()) + .httpStatusCode(HttpStatus.CREATED.value()) .build(); + + return ResponseEntity.status(HttpStatus.CREATED) + .body(apiResponse); } } \ No newline at end of file diff --git a/src/main/java/com/zufar/onlinestore/payment/entity/Payment.java b/src/main/java/com/zufar/onlinestore/payment/entity/Payment.java index 623c7f6c..c98135fa 100644 --- a/src/main/java/com/zufar/onlinestore/payment/entity/Payment.java +++ b/src/main/java/com/zufar/onlinestore/payment/entity/Payment.java @@ -16,7 +16,9 @@ import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; + import java.math.BigDecimal; +import java.util.UUID; @Getter @Setter @@ -34,6 +36,9 @@ public class Payment { @Column(name = "payment_intent_id", nullable = false, unique = true) private String paymentIntentId; + @Column(name = "shopping_session_id", nullable = false, unique = true) + private UUID shoppingSessionId; + @Column(name = "items_total_price", nullable = false) private BigDecimal itemsTotalPrice; diff --git a/src/main/java/com/zufar/onlinestore/payment/exception/CardTokenCreationException.java b/src/main/java/com/zufar/onlinestore/payment/exception/CardTokenCreationException.java new file mode 100644 index 00000000..af467be6 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/exception/CardTokenCreationException.java @@ -0,0 +1,14 @@ +package com.zufar.onlinestore.payment.exception; + +import lombok.Getter; + +@Getter +public class CardTokenCreationException extends RuntimeException { + + private final String cardNumber; + + public CardTokenCreationException(String cardNumber) { + super(String.format("Card token with cardNumber = %s cannot create", cardNumber)); + this.cardNumber = cardNumber; + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/exception/CardTokenRetrievingException.java b/src/main/java/com/zufar/onlinestore/payment/exception/CardTokenRetrievingException.java new file mode 100644 index 00000000..9835bb99 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/exception/CardTokenRetrievingException.java @@ -0,0 +1,14 @@ +package com.zufar.onlinestore.payment.exception; + +import lombok.Getter; + +@Getter +public class CardTokenRetrievingException extends RuntimeException { + + private final String cardDetailsTokenId; + + public CardTokenRetrievingException(String cardDetailsTokenId) { + super(String.format("Cannot retrieve stripe card token by cardDetailsTokenId = %s.", cardDetailsTokenId)); + this.cardDetailsTokenId = cardDetailsTokenId; + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/exception/CustomerRetrievingException.java b/src/main/java/com/zufar/onlinestore/payment/exception/CustomerRetrievingException.java new file mode 100644 index 00000000..39ad89b1 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/exception/CustomerRetrievingException.java @@ -0,0 +1,14 @@ +package com.zufar.onlinestore.payment.exception; + +import lombok.Getter; + +@Getter +public class CustomerRetrievingException extends RuntimeException { + + private final String stripeCustomerId; + + public CustomerRetrievingException(String stripeCustomerId) { + super(String.format("Cannot retrieve stripe customer by stripeCustomerId = %s.", stripeCustomerId)); + this.stripeCustomerId = stripeCustomerId; + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/exception/PaymentMethodNotFoundException.java b/src/main/java/com/zufar/onlinestore/payment/exception/PaymentMethodNotFoundException.java new file mode 100644 index 00000000..aee3fdf4 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/exception/PaymentMethodNotFoundException.java @@ -0,0 +1,14 @@ +package com.zufar.onlinestore.payment.exception; + +import lombok.Getter; + +@Getter +public class PaymentMethodNotFoundException extends RuntimeException { + + private final String stripeCustomerId; + + public PaymentMethodNotFoundException(String stripeCustomerId) { + super(String.format("The payment associated with customer is not found, stripeCustomerId = %s.", stripeCustomerId)); + this.stripeCustomerId = stripeCustomerId; + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/exception/PaymentMethodProcessingException.java b/src/main/java/com/zufar/onlinestore/payment/exception/PaymentMethodProcessingException.java index da2ddb3c..238023ec 100644 --- a/src/main/java/com/zufar/onlinestore/payment/exception/PaymentMethodProcessingException.java +++ b/src/main/java/com/zufar/onlinestore/payment/exception/PaymentMethodProcessingException.java @@ -8,7 +8,7 @@ public class PaymentMethodProcessingException extends RuntimeException { private final String paymentMethodType; public PaymentMethodProcessingException(String paymentMethodType) { - super(String.format("Payment method with the type = '%s' cannot be processed.", paymentMethodType)); + super(String.format("Cannot process payment method with type: %s.", paymentMethodType)); this.paymentMethodType = paymentMethodType; } } diff --git a/src/main/java/com/zufar/onlinestore/payment/exception/PaymentMethodRetrievingException.java b/src/main/java/com/zufar/onlinestore/payment/exception/PaymentMethodRetrievingException.java new file mode 100644 index 00000000..919e8e82 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/exception/PaymentMethodRetrievingException.java @@ -0,0 +1,15 @@ +package com.zufar.onlinestore.payment.exception; + +import lombok.Getter; + +@Getter +public class PaymentMethodRetrievingException extends RuntimeException { + + private final String stripeCustomerId; + + public PaymentMethodRetrievingException(String stripeCustomerId) { + super(String.format("Cannot retrieve payment method associated with stripe customer, " + + "stripeCustomerId = %s.", stripeCustomerId)); + this.stripeCustomerId = stripeCustomerId; + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/exception/ShoppingSessionAlreadyPaidException.java b/src/main/java/com/zufar/onlinestore/payment/exception/ShoppingSessionAlreadyPaidException.java new file mode 100644 index 00000000..8a7c7ce4 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/exception/ShoppingSessionAlreadyPaidException.java @@ -0,0 +1,16 @@ +package com.zufar.onlinestore.payment.exception; + +import lombok.Getter; + +import java.util.UUID; + +@Getter +public class ShoppingSessionAlreadyPaidException extends RuntimeException { + + private final UUID shoppingSessionId; + + public ShoppingSessionAlreadyPaidException(UUID shoppingSessionId) { + super(String.format("Shopping session with shoppingSessionId = %s is already paid", shoppingSessionId)); + this.shoppingSessionId = shoppingSessionId; + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/exception/StripeCustomerProcessingException.java b/src/main/java/com/zufar/onlinestore/payment/exception/StripeCustomerProcessingException.java new file mode 100644 index 00000000..abbca39f --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/exception/StripeCustomerProcessingException.java @@ -0,0 +1,14 @@ +package com.zufar.onlinestore.payment.exception; + +import lombok.Getter; + +@Getter +public class StripeCustomerProcessingException extends RuntimeException { + + private final String customerEmail; + + public StripeCustomerProcessingException(String customerEmail) { + super(String.format("Cannot process customer with email = %s.", customerEmail)); + this.customerEmail = customerEmail; + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/exception/handler/PaymentExceptionHandler.java b/src/main/java/com/zufar/onlinestore/payment/exception/handler/PaymentExceptionHandler.java index e2881c2b..f9a465c8 100644 --- a/src/main/java/com/zufar/onlinestore/payment/exception/handler/PaymentExceptionHandler.java +++ b/src/main/java/com/zufar/onlinestore/payment/exception/handler/PaymentExceptionHandler.java @@ -2,11 +2,17 @@ import com.zufar.onlinestore.common.exception.handler.GlobalExceptionHandler; import com.zufar.onlinestore.common.response.ApiResponse; +import com.zufar.onlinestore.payment.exception.PaymentNotFoundException; import com.zufar.onlinestore.payment.exception.PaymentEventProcessingException; import com.zufar.onlinestore.payment.exception.PaymentIntentProcessingException; -import com.zufar.onlinestore.payment.exception.PaymentNotFoundException; import com.zufar.onlinestore.payment.exception.PaymentMethodProcessingException; import com.zufar.onlinestore.payment.exception.PaymentEventParsingException; +import com.zufar.onlinestore.payment.exception.CardTokenCreationException; +import com.zufar.onlinestore.payment.exception.CardTokenRetrievingException; +import com.zufar.onlinestore.payment.exception.CustomerRetrievingException; +import com.zufar.onlinestore.payment.exception.PaymentMethodNotFoundException; +import com.zufar.onlinestore.payment.exception.PaymentMethodRetrievingException; +import com.zufar.onlinestore.payment.exception.ShoppingSessionAlreadyPaidException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; @@ -68,4 +74,64 @@ public ApiResponse handlePaymentEventParsingException(final PaymentEventPa return apiResponse; } + + @ExceptionHandler(CardTokenCreationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleCardTokenCreationException(final CardTokenCreationException exception) { + ApiResponse apiResponse = buildResponse(exception, HttpStatus.BAD_REQUEST); + log.error("Handle card token creation exception: failed: messages: {}, description: {}.", + apiResponse.messages(), apiResponse.description()); + + return apiResponse; + } + + @ExceptionHandler(CardTokenRetrievingException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleCardTokenRetrievingException(final CardTokenRetrievingException exception) { + ApiResponse apiResponse = buildResponse(exception, HttpStatus.BAD_REQUEST); + log.error("Handle card token retrieving exception: failed: messages: {}, description: {}.", + apiResponse.messages(), apiResponse.description()); + + return apiResponse; + } + + @ExceptionHandler(CustomerRetrievingException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleCustomerRetrievingException(final CustomerRetrievingException exception) { + ApiResponse apiResponse = buildResponse(exception, HttpStatus.BAD_REQUEST); + log.error("Handle customer retrieving exception: failed: messages: {}, description: {}.", + apiResponse.messages(), apiResponse.description()); + + return apiResponse; + } + + @ExceptionHandler(PaymentMethodNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ApiResponse handlePaymentMethodNotFoundException(final PaymentMethodNotFoundException exception) { + ApiResponse apiResponse = buildResponse(exception, HttpStatus.BAD_REQUEST); + log.error("Handle payment method not found exception: failed: messages: {}, description: {}.", + apiResponse.messages(), apiResponse.description()); + + return apiResponse; + } + + @ExceptionHandler(PaymentMethodRetrievingException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handlePaymentMethodRetrievingException(final PaymentMethodRetrievingException exception) { + ApiResponse apiResponse = buildResponse(exception, HttpStatus.BAD_REQUEST); + log.error("Handle payment method retrieving exception: failed: messages: {}, description: {}.", + apiResponse.messages(), apiResponse.description()); + + return apiResponse; + } + + @ExceptionHandler(ShoppingSessionAlreadyPaidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleShoppingSessionAlreadyPaidException(final ShoppingSessionAlreadyPaidException exception) { + ApiResponse apiResponse = buildResponse(exception, HttpStatus.BAD_REQUEST); + log.error("Handle shopping session already paid exception: failed: messages: {}, description: {}.", + apiResponse.messages(), apiResponse.description()); + + return apiResponse; + } } diff --git a/src/main/java/com/zufar/onlinestore/user/dto/UserDto.java b/src/main/java/com/zufar/onlinestore/user/dto/UserDto.java index 02bc04f1..ee0d29ec 100644 --- a/src/main/java/com/zufar/onlinestore/user/dto/UserDto.java +++ b/src/main/java/com/zufar/onlinestore/user/dto/UserDto.java @@ -10,6 +10,7 @@ public record UserDto( @JsonIgnore UUID userId, String firstName, String lastName, + String stripeCustomerToken, String username, String email, String password, diff --git a/src/main/java/com/zufar/onlinestore/user/entity/UserEntity.java b/src/main/java/com/zufar/onlinestore/user/entity/UserEntity.java index 26bfbd41..835b4df9 100644 --- a/src/main/java/com/zufar/onlinestore/user/entity/UserEntity.java +++ b/src/main/java/com/zufar/onlinestore/user/entity/UserEntity.java @@ -47,6 +47,9 @@ public class UserEntity implements UserDetails { @Column(name = "user_name", nullable = false) private String username; + @Column(name = "stripe_customer_token", nullable = true, unique = true) + private String stripeCustomerToken; + @Column(name = "email", nullable = false) private String email; diff --git a/src/main/resources/db/changelog/version-1.0/20.07.2023.create-payment-table.sql b/src/main/resources/db/changelog/version-1.0/20.07.2023.create-payment-table.sql index 7fcaa781..92cd86fc 100644 --- a/src/main/resources/db/changelog/version-1.0/20.07.2023.create-payment-table.sql +++ b/src/main/resources/db/changelog/version-1.0/20.07.2023.create-payment-table.sql @@ -1,9 +1,10 @@ CREATE TABLE IF NOT EXISTS payment ( payment_id BIGSERIAL PRIMARY KEY, - payment_intent_id VARCHAR(32) NOT NULL UNIQUE, - items_total_price DECIMAL NOT NULL CHECK (items_total_price > 0), + payment_intent_id VARCHAR(64) NOT NULL, + shopping_session_id UUID NOT NULL, + items_total_price DECIMAL NOT NULL CHECK (items_total_price > 0), status VARCHAR(32), - description TEXT - + description TEXT, + UNIQUE (payment_intent_id, shopping_session_id) ); \ No newline at end of file diff --git a/src/main/resources/db/changelog/version-1.0/28.07.2023.part4.create-user-details-table.sql b/src/main/resources/db/changelog/version-1.0/28.07.2023.part4.create-user-details-table.sql index 4ce32170..287ca836 100644 --- a/src/main/resources/db/changelog/version-1.0/28.07.2023.part4.create-user-details-table.sql +++ b/src/main/resources/db/changelog/version-1.0/28.07.2023.part4.create-user-details-table.sql @@ -3,6 +3,7 @@ CREATE TABLE user_details id UUID NOT NULL PRIMARY KEY, first_name VARCHAR(55) NOT NULL, last_name VARCHAR(55) NOT NULL, + stripe_customer_token VARCHAR(64), user_name VARCHAR(55) NOT NULL, email VARCHAR(55) NOT NULL, password VARCHAR(55) NOT NULL, @@ -11,6 +12,7 @@ CREATE TABLE user_details account_non_locked BOOLEAN NOT NULL, credentials_non_expired BOOLEAN NOT NULL, enabled BOOLEAN NOT NULL, + UNIQUE(stripe_customer_token), CONSTRAINT fk_address FOREIGN KEY (address_id) REFERENCES address (id)