diff --git a/src/main/java/com/zufar/onlinestore/payment/PaymentApi.java b/src/main/java/com/zufar/onlinestore/payment/PaymentApi.java deleted file mode 100644 index 0b01eed7..00000000 --- a/src/main/java/com/zufar/onlinestore/payment/PaymentApi.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.zufar.onlinestore.payment; - -import com.stripe.exception.StripeException; -import com.zufar.onlinestore.payment.dto.CreatePaymentDto; -import com.zufar.onlinestore.payment.dto.PaymentDetailsDto; -import com.zufar.onlinestore.payment.dto.PaymentDetailsWithTokenDto; -import com.zufar.onlinestore.payment.exception.PaymentNotFoundException; -import org.springframework.http.ResponseEntity; - -public interface PaymentApi { - - ResponseEntity paymentProcess(CreatePaymentDto paymentDto) throws StripeException; - - ResponseEntity getPaymentDetails(Long paymentId) throws PaymentNotFoundException; -} \ 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 new file mode 100644 index 00000000..bd146ff0 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/api/PaymentApi.java @@ -0,0 +1,49 @@ +package com.zufar.onlinestore.payment.api; + +import com.stripe.exception.SignatureVerificationException; +import com.stripe.exception.StripeException; +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.PaymentNotFoundException; + +public interface PaymentApi { + + /** + * This method allows to create a payment object + * + * @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 StripeException this is the parent of all Stripe API exceptions, in this method, exceptions descendants related to the payment creation and processing + * */ + PaymentDetailsWithTokenDto createPayment(final CreatePaymentDto createPaymentDto) throws StripeException; + + /** + * 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 StripeException this is the parent of all Stripe API exceptions, in this method, exceptions descendants related to the payment method creation and processing + * */ + String createPaymentMethod(final CreatePaymentMethodDto createPaymentMethodDto) throws StripeException; + + /** + * 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 exception throw then payment wasn't found + * */ + PaymentDetailsDto getPaymentDetails(final Long paymentId) throws PaymentNotFoundException; + + /** + * This method allows to create a payment method object + * + * @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 SignatureVerificationException this exception throw then stripe signature verification problem was occurred + * */ + void processPaymentEvent(final String paymentIntentPayload, final String stripeSignatureHeader) throws SignatureVerificationException; + +} \ No newline at end of file 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 new file mode 100644 index 00000000..969fc014 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/api/impl/PaymentApiImpl.java @@ -0,0 +1,45 @@ +package com.zufar.onlinestore.payment.api.impl; + +import com.stripe.exception.SignatureVerificationException; +import com.stripe.exception.StripeException; +import com.zufar.onlinestore.payment.api.PaymentApi; +import com.zufar.onlinestore.payment.dto.*; +import com.zufar.onlinestore.payment.exception.PaymentNotFoundException; +import com.zufar.onlinestore.payment.service.PaymentCreator; +import com.zufar.onlinestore.payment.service.event.PaymentEventProcessor; +import com.zufar.onlinestore.payment.service.PaymentMethodCreator; +import com.zufar.onlinestore.payment.service.PaymentRetriever; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PaymentApiImpl implements PaymentApi { + + private final PaymentRetriever paymentRetriever; + private final PaymentCreator paymentCreator; + private final PaymentMethodCreator paymentMethodCreator; + private final PaymentEventProcessor paymentEventProcessor; + + @Override + public PaymentDetailsWithTokenDto createPayment(final CreatePaymentDto createPaymentDto) throws StripeException { + return paymentCreator.createPayment(createPaymentDto); + } + + @Override + public String createPaymentMethod(final CreatePaymentMethodDto createPaymentMethodDto) throws StripeException { + return paymentMethodCreator.createPaymentMethod(createPaymentMethodDto); + } + + @Override + public PaymentDetailsDto getPaymentDetails(final Long paymentId) throws PaymentNotFoundException { + return paymentRetriever.getPaymentDetails(paymentId); + } + + @Override + public void processPaymentEvent(final String paymentIntentPayload, final String stripeSignatureHeader) throws SignatureVerificationException { + paymentEventProcessor.processPaymentEvent(paymentIntentPayload, stripeSignatureHeader); + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/calculator/PaymentPriceCalculator.java b/src/main/java/com/zufar/onlinestore/payment/calculator/PaymentPriceCalculator.java new file mode 100644 index 00000000..f6f1c7b2 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/calculator/PaymentPriceCalculator.java @@ -0,0 +1,21 @@ +package com.zufar.onlinestore.payment.calculator; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; + +@Slf4j +@Component +public class PaymentPriceCalculator { + + private static final Long COIN_TO_CURRENCY_CONVERSION_VALUE = 100L; + + public BigDecimal calculatePriceForPayment(Long totalPrice) { + return BigDecimal.valueOf(totalPrice / COIN_TO_CURRENCY_CONVERSION_VALUE); + } + + public Long calculatePriceForPaymentIntent(BigDecimal totalPrice) { + return totalPrice.longValue() * COIN_TO_CURRENCY_CONVERSION_VALUE; + } +} 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 b1428d60..383ab694 100644 --- a/src/main/java/com/zufar/onlinestore/payment/config/StripeConfiguration.java +++ b/src/main/java/com/zufar/onlinestore/payment/config/StripeConfiguration.java @@ -7,13 +7,17 @@ /** * 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 secretKey used to work with the Stripe Api from the backend side. - * @param publishableKey used to work with the Stripe Api from the frontend 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. + * */ + @ConfigurationProperties(prefix = "stripe") public record StripeConfiguration(String secretKey, - String publishableKey) { + String publishableKey, + String webHookSecretKey) { @PostConstruct private void init() { diff --git a/src/main/java/com/zufar/onlinestore/payment/controller/PaymentController.java b/src/main/java/com/zufar/onlinestore/payment/controller/PaymentController.java deleted file mode 100644 index 1f3ee1cd..00000000 --- a/src/main/java/com/zufar/onlinestore/payment/controller/PaymentController.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.zufar.onlinestore.payment.controller; - -import com.stripe.exception.StripeException; -import com.zufar.onlinestore.payment.PaymentApi; -import com.zufar.onlinestore.payment.dto.CreatePaymentDto; -import com.zufar.onlinestore.payment.dto.PaymentDetailsDto; -import com.zufar.onlinestore.payment.dto.PaymentDetailsWithTokenDto; -import com.zufar.onlinestore.payment.dto.PriceDetailsDto; -import com.zufar.onlinestore.payment.service.PaymentService; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -@Slf4j -@Validated -@RequiredArgsConstructor -@RestController -@RequestMapping(PaymentController.PAYMENT_URL) -public class PaymentController implements PaymentApi { - - public static final String PAYMENT_URL = "/api/v1/payment"; - - private final PaymentService paymentService; - - @PostMapping - public ResponseEntity paymentProcess(@RequestBody @Valid CreatePaymentDto paymentRequest) throws StripeException { - log.info("payment process: receive request to create payment: paymentRequest: {}.", paymentRequest); - PriceDetailsDto priceDetails = paymentRequest.priceDetails(); - PaymentDetailsWithTokenDto processedPayment = - paymentService.createPayment(paymentRequest.paymentMethodId(), priceDetails.totalPrice(), priceDetails.currency()); - log.info("payment process: payment successfully processed: processedPayment: {}.", processedPayment); - - return ResponseEntity.status(HttpStatus.CREATED) - .body(processedPayment); - } - - @GetMapping("/{paymentId}") - public ResponseEntity getPaymentDetails(@PathVariable Long paymentId) { - log.info("get payment details: receive payment id: paymentId: {}.", paymentId); - PaymentDetailsDto retrievedPayment = paymentService.getPayment(paymentId); - if (retrievedPayment == null) { - log.info("get payment details: not found payment details by id: paymentId: {}.", paymentId); - return ResponseEntity.notFound() - .build(); - } - log.info("get payment details: payment successfully retrieved: retrievedPayment: {}.", retrievedPayment); - - return ResponseEntity.ok() - .body(retrievedPayment); - } -} \ No newline at end of file diff --git a/src/main/java/com/zufar/onlinestore/payment/mapper/PaymentConverter.java b/src/main/java/com/zufar/onlinestore/payment/converter/PaymentConverter.java similarity index 74% rename from src/main/java/com/zufar/onlinestore/payment/mapper/PaymentConverter.java rename to src/main/java/com/zufar/onlinestore/payment/converter/PaymentConverter.java index 2d031be7..83352ff4 100644 --- a/src/main/java/com/zufar/onlinestore/payment/mapper/PaymentConverter.java +++ b/src/main/java/com/zufar/onlinestore/payment/converter/PaymentConverter.java @@ -1,4 +1,4 @@ -package com.zufar.onlinestore.payment.mapper; +package com.zufar.onlinestore.payment.converter; import com.zufar.onlinestore.payment.dto.PaymentDetailsDto; import com.zufar.onlinestore.payment.entity.Payment; @@ -7,9 +7,10 @@ @Component public class PaymentConverter { - public PaymentDetailsDto toDto(Payment entity) { + public PaymentDetailsDto toDto(final Payment entity) { return PaymentDetailsDto.builder() .paymentId(entity.getPaymentId()) + .paymentIntentId(entity.getPaymentIntentId()) .totalPrice(entity.getItemsTotalPrice()) .currency(entity.getCurrency()) .description(entity.getDescription()) @@ -17,13 +18,15 @@ public PaymentDetailsDto toDto(Payment entity) { .build(); } - public Payment toEntity(PaymentDetailsDto dto) { + public Payment toEntity(final PaymentDetailsDto dto) { return Payment.builder() .paymentId(dto.paymentId()) + .paymentIntentId(dto.paymentIntentId()) .itemsTotalPrice(dto.totalPrice()) .currency(dto.currency()) .description(dto.description()) .status(dto.status()) .build(); } + } diff --git a/src/main/java/com/zufar/onlinestore/payment/converter/PaymentIntentConverter.java b/src/main/java/com/zufar/onlinestore/payment/converter/PaymentIntentConverter.java new file mode 100644 index 00000000..e5d8ff81 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/converter/PaymentIntentConverter.java @@ -0,0 +1,33 @@ +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 lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class PaymentIntentConverter { + + private final PaymentPriceCalculator paymentPriceCalculator; + + public Payment toPayment(final PaymentIntent paymentIntent) { + return Payment.builder() + .itemsTotalPrice(paymentPriceCalculator.calculatePriceForPayment(paymentIntent.getAmount())) + .paymentIntentId(paymentIntent.getId()) + .currency(paymentIntent.getCurrency()) + .build(); + } + + public PaymentIntentCreateParams toPaymentIntentParams(final CreatePaymentDto createPaymentDto) { + return PaymentIntentCreateParams.builder() + .setAmount(paymentPriceCalculator.calculatePriceForPaymentIntent( + createPaymentDto.priceDetails().totalPrice())) + .setCurrency( createPaymentDto.priceDetails().currency()) + .setPaymentMethod(createPaymentDto.paymentMethodId()) + .build(); + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/converter/PaymentMethodConverter.java b/src/main/java/com/zufar/onlinestore/payment/converter/PaymentMethodConverter.java new file mode 100644 index 00000000..0a8cfa31 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/converter/PaymentMethodConverter.java @@ -0,0 +1,23 @@ +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.CardDetails cardDetails = PaymentMethodCreateParams.CardDetails.builder() + .setNumber(createPaymentMethodDto.cardNumber()) + .setExpMonth(createPaymentMethodDto.expMonth()) + .setExpYear(createPaymentMethodDto.expYear()) + .setCvc(createPaymentMethodDto.cvc()) + .build(); + + return PaymentMethodCreateParams.builder() + .setCard(cardDetails) + .setType(PaymentMethodCreateParams.Type.CARD) + .build(); + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/dto/CreatePaymentDto.java b/src/main/java/com/zufar/onlinestore/payment/dto/CreatePaymentDto.java index b6d3db75..5c3144b8 100644 --- a/src/main/java/com/zufar/onlinestore/payment/dto/CreatePaymentDto.java +++ b/src/main/java/com/zufar/onlinestore/payment/dto/CreatePaymentDto.java @@ -5,9 +5,10 @@ public record CreatePaymentDto( - @NotBlank(message = "paymentMethodId is the mandatory attribute") + @NotBlank(message = "PaymentMethodId is the mandatory attribute") String paymentMethodId, @NotNull(message = "PriceDetails is the mandatory attribute") - PriceDetailsDto priceDetails) { + PriceDetailsDto priceDetails +) { } diff --git a/src/main/java/com/zufar/onlinestore/payment/dto/CreatePaymentMethodDto.java b/src/main/java/com/zufar/onlinestore/payment/dto/CreatePaymentMethodDto.java new file mode 100644 index 00000000..71ba734b --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/dto/CreatePaymentMethodDto.java @@ -0,0 +1,20 @@ +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/PaymentDetailsDto.java b/src/main/java/com/zufar/onlinestore/payment/dto/PaymentDetailsDto.java index 46dda621..c53caa5e 100644 --- a/src/main/java/com/zufar/onlinestore/payment/dto/PaymentDetailsDto.java +++ b/src/main/java/com/zufar/onlinestore/payment/dto/PaymentDetailsDto.java @@ -1,13 +1,16 @@ package com.zufar.onlinestore.payment.dto; +import com.zufar.onlinestore.payment.enums.PaymentStatus; import lombok.Builder; - import java.math.BigDecimal; @Builder -public record PaymentDetailsDto(Long paymentId, - BigDecimal totalPrice, - String currency, - String status, - String description) { +public record PaymentDetailsDto( + Long paymentId, + BigDecimal totalPrice, + String paymentIntentId, + String currency, + PaymentStatus status, + String description +) { } diff --git a/src/main/java/com/zufar/onlinestore/payment/dto/PaymentDetailsWithTokenDto.java b/src/main/java/com/zufar/onlinestore/payment/dto/PaymentDetailsWithTokenDto.java index b6833be6..7c267776 100644 --- a/src/main/java/com/zufar/onlinestore/payment/dto/PaymentDetailsWithTokenDto.java +++ b/src/main/java/com/zufar/onlinestore/payment/dto/PaymentDetailsWithTokenDto.java @@ -3,6 +3,8 @@ import lombok.Builder; @Builder -public record PaymentDetailsWithTokenDto(String paymentToken, - PaymentDetailsDto paymentDetailsDto) { +public record PaymentDetailsWithTokenDto( + String paymentToken, + PaymentDetailsDto paymentDetailsDto +) { } diff --git a/src/main/java/com/zufar/onlinestore/payment/dto/PaymentWithTokenDetailsDto.java b/src/main/java/com/zufar/onlinestore/payment/dto/PaymentWithTokenDetailsDto.java deleted file mode 100644 index 39e9f196..00000000 --- a/src/main/java/com/zufar/onlinestore/payment/dto/PaymentWithTokenDetailsDto.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.zufar.onlinestore.payment.dto; - -import lombok.Builder; - -@Builder -public record PaymentWithTokenDetailsDto(String paymentToken, - PaymentDetailsDto paymentDetailsDto) { -} diff --git a/src/main/java/com/zufar/onlinestore/payment/dto/PriceDetailsDto.java b/src/main/java/com/zufar/onlinestore/payment/dto/PriceDetailsDto.java index 4456e902..289173f1 100644 --- a/src/main/java/com/zufar/onlinestore/payment/dto/PriceDetailsDto.java +++ b/src/main/java/com/zufar/onlinestore/payment/dto/PriceDetailsDto.java @@ -13,5 +13,6 @@ public record PriceDetailsDto( @NotBlank(message = "Currency is mandatory attribute") @Size(min = 3, max = 3, message = "Currency value must be only 3 characters long") - String currency) { + String currency +) { } \ No newline at end of file diff --git a/src/main/java/com/zufar/onlinestore/payment/endpoint/PaymentEndpoint.java b/src/main/java/com/zufar/onlinestore/payment/endpoint/PaymentEndpoint.java new file mode 100644 index 00000000..bec1471c --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/endpoint/PaymentEndpoint.java @@ -0,0 +1,81 @@ +package com.zufar.onlinestore.payment.endpoint; + +import com.stripe.exception.SignatureVerificationException; +import com.stripe.exception.StripeException; +import com.zufar.onlinestore.payment.api.PaymentApi; +import com.zufar.onlinestore.payment.dto.*; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +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.RestController; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; + +import java.util.Objects; + +@Slf4j +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping(PaymentEndpoint.PAYMENT_URL) +public class PaymentEndpoint { + + public static final String PAYMENT_URL = "/api/v1/payment"; + + private final PaymentApi paymentApi; + + @PostMapping + public ResponseEntity createPayment(@RequestBody @Valid final CreatePaymentDto createPaymentDto) throws StripeException { + PaymentDetailsWithTokenDto createdPayment = paymentApi.createPayment(createPaymentDto); + return ResponseEntity.status(HttpStatus.CREATED) + .body(createdPayment); + } + + @GetMapping("/{paymentId}") + public ResponseEntity getPaymentDetails(@PathVariable @Valid @NotNull Long paymentId) { + PaymentDetailsDto retrievedPayment = paymentApi.getPaymentDetails(paymentId); + if (Objects.isNull(retrievedPayment)) { + log.info("Get payment details: not found payment details by id: {}.", paymentId); + return ResponseEntity.notFound() + .build(); + } + log.info("Get payment details: payment details: {} successfully retrieved.", retrievedPayment); + + return ResponseEntity.ok() + .body(retrievedPayment); + } + + /** + * 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 StripeException { + String paymentMethodId = paymentApi.createPaymentMethod(createPaymentMethodDto); + return ResponseEntity.ok() + .body(paymentMethodId); + } + + @PostMapping("/event") + public ResponseEntity paymentEventsProcess( + @RequestBody @Valid @NotEmpty @NotNull String paymentIntentPayload, + @RequestHeader("Stripe-Signature") @Valid @NotEmpty @NotNull String stripeSignatureHeader) throws SignatureVerificationException { + if (Objects.isNull(paymentIntentPayload) || Objects.isNull(stripeSignatureHeader)) { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + paymentApi.processPaymentEvent(paymentIntentPayload, stripeSignatureHeader); + + return ResponseEntity.ok() + .build(); + } +} \ 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 f6abf5b6..6b608737 100644 --- a/src/main/java/com/zufar/onlinestore/payment/entity/Payment.java +++ b/src/main/java/com/zufar/onlinestore/payment/entity/Payment.java @@ -1,15 +1,22 @@ package com.zufar.onlinestore.payment.entity; +import com.zufar.onlinestore.payment.enums.PaymentStatus; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Column; +import jakarta.persistence.Enumerated; +import jakarta.persistence.EnumType; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.Getter; +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; @Getter @@ -25,15 +32,44 @@ public class Payment { @Column(name = "payment_id") private Long paymentId; - @Column(nullable = false, name = "currency") + @Column(name = "payment_intent_id", nullable = false, unique = true) + private String paymentIntentId; + + @Column(name = "currency", nullable = false) private String currency; - @Column(nullable = false, name = "items_total_price") + @Column(name = "items_total_price", nullable = false) private BigDecimal itemsTotalPrice; - @Column(name = "status") - private String status; + @Column(name = "status", nullable = true) + @Enumerated(value = EnumType.STRING) + private PaymentStatus status; - @Column(name = "description") + @Column(name = "description", nullable = true) private String description; + + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Payment payment)) { + return false; + } + EqualsBuilder eb = new EqualsBuilder(); + eb.append(paymentIntentId, payment.paymentId); + return eb.isEquals(); + } + + public int hashCode() { + HashCodeBuilder hcb = new HashCodeBuilder(); + hcb.append(paymentIntentId); + return hcb.toHashCode(); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .append("paymentIntentId", paymentIntentId) + .toString(); + } } diff --git a/src/main/java/com/zufar/onlinestore/payment/enums/PaymentStatus.java b/src/main/java/com/zufar/onlinestore/payment/enums/PaymentStatus.java index 09b4a344..92fc4a37 100644 --- a/src/main/java/com/zufar/onlinestore/payment/enums/PaymentStatus.java +++ b/src/main/java/com/zufar/onlinestore/payment/enums/PaymentStatus.java @@ -1,12 +1,37 @@ package com.zufar.onlinestore.payment.enums; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor public enum PaymentStatus { - REQUIRES_PAYMENT_METHOD, - REQUIRES_CONFIRMATION, - REQUIRES_CAPTURE, - REQUIRES_ACTION, - PROCESSING, - CANCELED, - SUCCEEDED + + PAYMENT_METHOD_IS_REQUIRED("payment_intent.requires_payment_method", + "Payment method is required"), + + PAYMENT_CONFIRMATION_IS_REQUIRED("payment_intent.requires_confirmation", + "Payment confirmation is required"), + + PAYMENT_CAPTURE_IS_REQUIRED("payment_intent.requires_capture", + "Payment capture is required"), + + PAYMENT_ACTION_IS_REQUIRED("payment_intent.requires_action", + "Additional action is required to complete payment"), + + PAYMENT_IS_FAILED("payment_intent.payment_failed", + "Payment processing error"), + + PAYMENT_IS_PROCESSING("payment_intent.processing", + "Payment in processing"), + + PAYMENT_IS_CANCELED("payment_intent.canceled", + "Payment is canceled"), + + PAYMENT_IS_SUCCEEDED("payment_intent.succeeded", + "Payment is succeeded"); + + private final String status; + private final String description; } diff --git a/src/main/java/com/zufar/onlinestore/payment/exception/UnexpectedPaymentStatusException.java b/src/main/java/com/zufar/onlinestore/payment/exception/UnexpectedPaymentStatusException.java new file mode 100644 index 00000000..7b17f7ec --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/exception/UnexpectedPaymentStatusException.java @@ -0,0 +1,15 @@ +package com.zufar.onlinestore.payment.exception; + +import com.zufar.onlinestore.payment.enums.PaymentStatus; +import lombok.Getter; + +@Getter +public class UnexpectedPaymentStatusException extends RuntimeException { + + private final PaymentStatus paymentStatus; + + public UnexpectedPaymentStatusException(final PaymentStatus paymentStatus) { + super(String.format("Payment status: %s is unexpected.", paymentStatus)); + this.paymentStatus = paymentStatus; + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/exception/UnsupportedScenarioExecutorException.java b/src/main/java/com/zufar/onlinestore/payment/exception/UnsupportedScenarioExecutorException.java new file mode 100644 index 00000000..9498b6cb --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/exception/UnsupportedScenarioExecutorException.java @@ -0,0 +1,15 @@ +package com.zufar.onlinestore.payment.exception; + +import com.stripe.model.Event; +import lombok.Getter; + +@Getter +public class UnsupportedScenarioExecutorException extends RuntimeException { + + private final Event event; + + public UnsupportedScenarioExecutorException(final Event event) { + super(String.format("Scenario executor for event: %s is unsupported.", event)); + this.event = event; + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/processor/PaymentProcessor.java b/src/main/java/com/zufar/onlinestore/payment/processor/PaymentProcessor.java deleted file mode 100644 index eb0ff51b..00000000 --- a/src/main/java/com/zufar/onlinestore/payment/processor/PaymentProcessor.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.zufar.onlinestore.payment.processor; - -import com.stripe.exception.StripeException; -import com.stripe.model.PaymentIntent; -import com.stripe.model.PaymentMethod; -import com.stripe.param.PaymentIntentCreateParams; -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.Component; - -import java.math.BigDecimal; - -@Slf4j -@RequiredArgsConstructor -@Component -public class PaymentProcessor { - - public static final String PAYMENT_MESSAGE = "Payment made by user: %s, using the payment method: %s."; - public static final Integer PAYMENT_DELIMITER = 100; - - private final StripeConfiguration stripeConfig; - - public Pair process(String paymentMethodId, BigDecimal totalPrice, String currency) throws StripeException { - PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentMethodId); - PaymentIntentCreateParams params = getPaymentParams(paymentMethod, totalPrice, currency); - log.info("process: get payment intent params for payment creation: params: {}.", params); - PaymentIntent paymentIntent = PaymentIntent.create(params); - log.info("process: payment intent successfully created: paymentIntentId: {}.", paymentIntent.getId()); - String paymentToken = paymentIntent.getClientSecret(); - Payment payment = getProcessedPayment(paymentIntent); - - return Pair.of(paymentToken, payment); - } - - private PaymentIntentCreateParams getPaymentParams(PaymentMethod paymentMethod, BigDecimal totalPrice, String currency) { - String email = paymentMethod.getBillingDetails().getEmail(); - return PaymentIntentCreateParams.builder() - .setAmount(totalPrice.longValue() * PAYMENT_DELIMITER) - .setCurrency(currency) - .setPaymentMethod(paymentMethod.getId()) - .setReceiptEmail(email) - .setDescription(PAYMENT_MESSAGE.formatted(email, paymentMethod.getType())) - .build(); - } - - private Payment getProcessedPayment(PaymentIntent paymentIntent) { - return Payment.builder() - .itemsTotalPrice(BigDecimal.valueOf(paymentIntent.getAmount() / PAYMENT_DELIMITER)) - .currency(paymentIntent.getCurrency()) - .description(paymentIntent.getDescription()) - .status(paymentIntent.getStatus()) - .build(); - } -} \ No newline at end of file diff --git a/src/main/java/com/zufar/onlinestore/payment/repository/PaymentRepository.java b/src/main/java/com/zufar/onlinestore/payment/repository/PaymentRepository.java index 8769b7cf..ac0aa6ad 100644 --- a/src/main/java/com/zufar/onlinestore/payment/repository/PaymentRepository.java +++ b/src/main/java/com/zufar/onlinestore/payment/repository/PaymentRepository.java @@ -1,7 +1,21 @@ package com.zufar.onlinestore.payment.repository; import com.zufar.onlinestore.payment.entity.Payment; +import com.zufar.onlinestore.payment.enums.PaymentStatus; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +@Repository public interface PaymentRepository extends CrudRepository { + + @Modifying + @Query(value = "UPDATE payment SET status = :payment_status, description = :payment_description WHERE payment_intent_id = :payment_intent_id", + nativeQuery = true) + void updateStatusAndDescriptionInPayment(@Param("payment_intent_id") String paymentIntentId, + @Param("payment_status") String paymentStatus, + @Param("payment_description") String paymentDescription); + } diff --git a/src/main/java/com/zufar/onlinestore/payment/service/PaymentCreator.java b/src/main/java/com/zufar/onlinestore/payment/service/PaymentCreator.java new file mode 100644 index 00000000..3cb14cae --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/service/PaymentCreator.java @@ -0,0 +1,49 @@ +package com.zufar.onlinestore.payment.service; + +import com.stripe.exception.StripeException; +import com.stripe.model.PaymentIntent; +import com.zufar.onlinestore.payment.converter.PaymentIntentConverter; +import com.zufar.onlinestore.payment.dto.CreatePaymentDto; +import com.zufar.onlinestore.payment.dto.PaymentDetailsWithTokenDto; +import com.zufar.onlinestore.payment.entity.Payment; +import com.zufar.onlinestore.payment.converter.PaymentConverter; +import com.zufar.onlinestore.payment.repository.PaymentRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +/** + * 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 + * and Stripe API sides to process payment. + * */ + +@Slf4j +@RequiredArgsConstructor +@Service +public class PaymentCreator { + + private final PaymentRepository paymentRepository; + private final PaymentIntentCreator paymentIntentCreator; + private final PaymentIntentConverter paymentIntentConverter; + private final PaymentConverter paymentConverter; + + @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED) + public PaymentDetailsWithTokenDto createPayment(final CreatePaymentDto createPaymentDto) throws StripeException { + 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(); + } + +} diff --git a/src/main/java/com/zufar/onlinestore/payment/service/PaymentIntentCreator.java b/src/main/java/com/zufar/onlinestore/payment/service/PaymentIntentCreator.java new file mode 100644 index 00000000..c0f19b6f --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/service/PaymentIntentCreator.java @@ -0,0 +1,37 @@ +package com.zufar.onlinestore.payment.service; + +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 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 StripeException { + Stripe.apiKey = stripeConfig.secretKey(); + + PaymentIntentCreateParams params = paymentIntentConverter.toPaymentIntentParams(createPaymentDto); + log.info("Create payment intent: payment intent params: {} for creation.", params); + + return PaymentIntent.create(params); + } + +} diff --git a/src/main/java/com/zufar/onlinestore/payment/service/PaymentMethodCreator.java b/src/main/java/com/zufar/onlinestore/payment/service/PaymentMethodCreator.java new file mode 100644 index 00000000..4628c1cc --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/service/PaymentMethodCreator.java @@ -0,0 +1,37 @@ +package com.zufar.onlinestore.payment.service; + +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 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 StripeException { + Stripe.apiKey = stripeConfig.publishableKey(); + + PaymentMethodCreateParams params = paymentMethodConverter.toPaymentMethodParams(createPaymentMethodDto); + PaymentMethod paymentMethod = PaymentMethod.create(params); + 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/service/PaymentRetriever.java b/src/main/java/com/zufar/onlinestore/payment/service/PaymentRetriever.java new file mode 100644 index 00000000..8bd88aae --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/service/PaymentRetriever.java @@ -0,0 +1,37 @@ +package com.zufar.onlinestore.payment.service; + +import com.zufar.onlinestore.payment.dto.PaymentDetailsDto; +import com.zufar.onlinestore.payment.exception.PaymentNotFoundException; +import com.zufar.onlinestore.payment.converter.PaymentConverter; +import com.zufar.onlinestore.payment.repository.PaymentRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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.Objects; + +/** + * This class is responsible for retrieving relevant payment details from database + * */ + +@Slf4j +@RequiredArgsConstructor +@Service +public class PaymentRetriever { + + private final PaymentRepository paymentRepository; + private final PaymentConverter paymentConverter; + + @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED) + public PaymentDetailsDto getPaymentDetails(Long paymentId) { + Objects.requireNonNull(paymentId); + log.info("Get payment details: start payment details retrieve by payment id: {}.", paymentId); + + return paymentRepository.findById(paymentId) + .map(paymentConverter::toDto) + .orElseThrow(() -> new PaymentNotFoundException(paymentId)); + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/service/PaymentService.java b/src/main/java/com/zufar/onlinestore/payment/service/PaymentService.java deleted file mode 100644 index 8c31b9d8..00000000 --- a/src/main/java/com/zufar/onlinestore/payment/service/PaymentService.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.zufar.onlinestore.payment.service; - -import com.stripe.exception.StripeException; -import com.zufar.onlinestore.payment.dto.PaymentDetailsDto; -import com.zufar.onlinestore.payment.dto.PaymentDetailsWithTokenDto; -import com.zufar.onlinestore.payment.exception.PaymentNotFoundException; - -import java.math.BigDecimal; - -public interface PaymentService { - - PaymentDetailsWithTokenDto createPayment(String paymentMethodId, - BigDecimal totalPrice, - String currency) throws StripeException; - - PaymentDetailsDto getPayment(Long paymentId) throws PaymentNotFoundException; -} - diff --git a/src/main/java/com/zufar/onlinestore/payment/service/event/PaymentEventCreator.java b/src/main/java/com/zufar/onlinestore/payment/service/event/PaymentEventCreator.java new file mode 100644 index 00000000..0b1636ca --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/service/event/PaymentEventCreator.java @@ -0,0 +1,27 @@ +package com.zufar.onlinestore.payment.service.event; + +import com.stripe.exception.SignatureVerificationException; +import com.stripe.model.Event; +import com.stripe.net.Webhook; +import com.zufar.onlinestore.payment.config.StripeConfiguration; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * This class is responsible for payment event (stripe object) creation. + * */ + +@Slf4j +@RequiredArgsConstructor +@Service +public class PaymentEventCreator { + + private final StripeConfiguration stripeConfig; + + public Event createPaymentEvent(String paymentIntentPayload, String stripeSignatureHeader) throws SignatureVerificationException { + log.info("Create payment event: start payment event creation:" + + " paymentIntentPayload: {}, stripeSignatureHeader: {}.", paymentIntentPayload, stripeSignatureHeader); + return Webhook.constructEvent(paymentIntentPayload, stripeSignatureHeader, stripeConfig.webHookSecretKey()); + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/service/event/PaymentEventHandler.java b/src/main/java/com/zufar/onlinestore/payment/service/event/PaymentEventHandler.java new file mode 100644 index 00000000..01228dec --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/service/event/PaymentEventHandler.java @@ -0,0 +1,39 @@ +package com.zufar.onlinestore.payment.service.event; + +import com.stripe.model.Event; +import com.stripe.model.PaymentIntent; +import com.zufar.onlinestore.payment.exception.UnsupportedScenarioExecutorException; +import com.zufar.onlinestore.payment.service.scenario.PaymentScenarioExecutor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Objects; + +/** + * This class is responsible for catching payment event type, comparing it with existing + * payment statuses and based on their correspondence, calling the desired scenario handler. + */ + +@Slf4j +@RequiredArgsConstructor +@Service +public class PaymentEventHandler { + + private final List executors; + + public void handlePaymentEvent(Event event, PaymentIntent paymentIntent) { + log.debug("Handle payment event method: input parameters: event: {}, paymentIntent: {}.", event, paymentIntent); + if (Objects.nonNull(paymentIntent) && Objects.nonNull(event)) { + log.info("Handle payment event method: start of handling payment event"); + + executors.stream() + .filter(executor -> executor.supports(event)) + .findFirst() + .ifPresent(executor -> executor.execute(paymentIntent)); + + log.info("Handle payment event method: finish of handling payment event"); + } + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/service/event/PaymentEventParser.java b/src/main/java/com/zufar/onlinestore/payment/service/event/PaymentEventParser.java new file mode 100644 index 00000000..48451cd9 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/service/event/PaymentEventParser.java @@ -0,0 +1,25 @@ +package com.zufar.onlinestore.payment.service.event; + +import com.stripe.model.Event; +import com.stripe.model.EventDataObjectDeserializer; +import com.stripe.model.PaymentIntent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * This class is responsible for parsing payment event to payment intent object. + * */ + +@Slf4j +@Service +public class PaymentEventParser { + + public PaymentIntent parseEventToPaymentIntent(final Event event) { + log.info("Parse event to payment intent: start payment event deserialization: event: {}", event); + EventDataObjectDeserializer dataObjectDeserializer = event.getDataObjectDeserializer(); + return dataObjectDeserializer.getObject() + .filter(PaymentIntent.class::isInstance) + .map(PaymentIntent.class::cast) + .orElse(new PaymentIntent()); + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/service/event/PaymentEventProcessor.java b/src/main/java/com/zufar/onlinestore/payment/service/event/PaymentEventProcessor.java new file mode 100644 index 00000000..8f09bf72 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/service/event/PaymentEventProcessor.java @@ -0,0 +1,33 @@ +package com.zufar.onlinestore.payment.service.event; + +import com.stripe.exception.SignatureVerificationException; +import com.stripe.model.Event; +import com.stripe.model.PaymentIntent; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * This class is responsible for processing payment event to transfer it to the responsibility area + * of class that is engaged in catching event types. + * */ + +@Slf4j +@RequiredArgsConstructor +@Service +public class PaymentEventProcessor { + + private final PaymentEventCreator paymentEventCreator; + private final PaymentEventParser paymentEventParser; + private final PaymentEventHandler paymentEventHandler; + + public void processPaymentEvent(String paymentIntentPayload, String stripeSignatureHeader) throws SignatureVerificationException { + log.info("Process payment event: start payment event processing: input params paymentIntentPayload: {}," + + "stripeSignatureHeader: {}.", paymentIntentPayload, stripeSignatureHeader); + Event event = paymentEventCreator.createPaymentEvent(paymentIntentPayload, stripeSignatureHeader); + PaymentIntent paymentIntent = paymentEventParser.parseEventToPaymentIntent(event); + paymentEventHandler.handlePaymentEvent(event, paymentIntent); + log.info("Process payment event: event successfully processed"); + } + +} diff --git a/src/main/java/com/zufar/onlinestore/payment/service/impl/PaymentServiceImpl.java b/src/main/java/com/zufar/onlinestore/payment/service/impl/PaymentServiceImpl.java deleted file mode 100644 index e96feb9d..00000000 --- a/src/main/java/com/zufar/onlinestore/payment/service/impl/PaymentServiceImpl.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.zufar.onlinestore.payment.service.impl; - -import com.stripe.exception.StripeException; -import com.zufar.onlinestore.payment.dto.PaymentDetailsWithTokenDto; -import com.zufar.onlinestore.payment.dto.PaymentDetailsDto; -import com.zufar.onlinestore.payment.entity.Payment; -import com.zufar.onlinestore.payment.exception.PaymentNotFoundException; -import com.zufar.onlinestore.payment.mapper.PaymentConverter; -import com.zufar.onlinestore.payment.processor.PaymentProcessor; -import com.zufar.onlinestore.payment.repository.PaymentRepository; -import com.zufar.onlinestore.payment.service.PaymentService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.tuple.Pair; -import org.springframework.stereotype.Service; -import java.math.BigDecimal; - -@Slf4j -@RequiredArgsConstructor -@Service -public class PaymentServiceImpl implements PaymentService { - - private final PaymentProcessor paymentProcessor; - private final PaymentRepository paymentRepository; - private final PaymentConverter paymentConverter; - - public PaymentDetailsWithTokenDto createPayment(String paymentMethodId, BigDecimal totalPrice, String currency) throws StripeException { - Pair paymentWithTokenDetails = paymentProcessor.process(paymentMethodId, totalPrice, currency); - log.info("create payment: payment successfully processed: paymentWithTokenDetails: {}.", paymentWithTokenDetails); - Payment savedPayment = paymentRepository.save(paymentWithTokenDetails.getValue()); - log.info("create payment: payment successfully saved: savedPayment: {}.", savedPayment); - return PaymentDetailsWithTokenDto.builder() - .paymentToken(paymentWithTokenDetails.getKey()) - .paymentDetailsDto((paymentConverter.toDto(savedPayment))) - .build(); - } - - public PaymentDetailsDto getPayment(Long paymentId) { - return paymentRepository.findById(paymentId) - .map(paymentConverter::toDto) - .orElseThrow(() -> new PaymentNotFoundException(paymentId)); - } -} diff --git a/src/main/java/com/zufar/onlinestore/payment/service/scenario/PaymentFailedScenarioExecutor.java b/src/main/java/com/zufar/onlinestore/payment/service/scenario/PaymentFailedScenarioExecutor.java new file mode 100644 index 00000000..e38dbb4c --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/service/scenario/PaymentFailedScenarioExecutor.java @@ -0,0 +1,45 @@ +package com.zufar.onlinestore.payment.service.scenario; + +import com.stripe.model.Event; +import com.stripe.model.PaymentIntent; +import com.zufar.onlinestore.payment.entity.Payment; +import com.zufar.onlinestore.payment.enums.PaymentStatus; +import com.zufar.onlinestore.payment.repository.PaymentRepository; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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.Arrays; +import java.util.Objects; + +/** + * This class is responsible for handling the fail scenario and updating + * in database record of payment, with the relevant status and description + * */ + +@Slf4j +@RequiredArgsConstructor +@Service +public class PaymentFailedScenarioExecutor implements PaymentScenarioExecutor { + + private final PaymentRepository paymentRepository; + + private static final PaymentStatus paymentStatus = PaymentStatus.PAYMENT_IS_FAILED; + + @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED) + public void execute(PaymentIntent paymentIntent) { + log.info("Handle payment scenario method: start of handling payment intent: {} by failed scenario.", paymentIntent); + paymentRepository.updateStatusAndDescriptionInPayment(paymentIntent.getId(), paymentStatus.toString(), paymentStatus.getDescription()); + log.info("Handle payment scenario method: finish of handling payment intent: {} by failed scenario.", paymentIntent); + } + + @Override + public boolean supports(Event event) { + return Objects.equals(paymentStatus.getStatus(), event.getType()); + } +} diff --git a/src/main/java/com/zufar/onlinestore/payment/service/scenario/PaymentScenarioExecutor.java b/src/main/java/com/zufar/onlinestore/payment/service/scenario/PaymentScenarioExecutor.java new file mode 100644 index 00000000..9e1ca08e --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/service/scenario/PaymentScenarioExecutor.java @@ -0,0 +1,12 @@ +package com.zufar.onlinestore.payment.service.scenario; + +import com.stripe.model.Event; +import com.stripe.model.PaymentIntent; + +public interface PaymentScenarioExecutor { + + void execute(final PaymentIntent paymentIntent); + + boolean supports(final Event event); + +} diff --git a/src/main/java/com/zufar/onlinestore/payment/service/scenario/PaymentSuccessfulScenarioExecutor.java b/src/main/java/com/zufar/onlinestore/payment/service/scenario/PaymentSuccessfulScenarioExecutor.java new file mode 100644 index 00000000..09dcf8d4 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/payment/service/scenario/PaymentSuccessfulScenarioExecutor.java @@ -0,0 +1,42 @@ +package com.zufar.onlinestore.payment.service.scenario; + +import com.stripe.model.Event; +import com.stripe.model.PaymentIntent; +import com.zufar.onlinestore.payment.entity.Payment; +import com.zufar.onlinestore.payment.enums.PaymentStatus; +import com.zufar.onlinestore.payment.repository.PaymentRepository; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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.Objects; + +/** + * This class is responsible for handling the successful scenario and updating + * in database record of payment, with the relevant status and description + * */ + +@Slf4j +@RequiredArgsConstructor +@Service +public class PaymentSuccessfulScenarioExecutor implements PaymentScenarioExecutor { + + private final PaymentRepository paymentRepository; + + private static final PaymentStatus paymentStatus = PaymentStatus.PAYMENT_IS_SUCCEEDED; + + @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED) + public void execute(PaymentIntent paymentIntent) { + log.info("Handle payment scenario method: start of handling payment intent: {} by successful scenario.", paymentIntent); + paymentRepository.updateStatusAndDescriptionInPayment(paymentIntent.getId(), paymentStatus.toString(), paymentStatus.getDescription()); + log.info("Handle payment scenario method: finish of handling payment intent: {} by successful scenario.", paymentIntent); + } + + @Override + public boolean supports(Event event) {return Objects.equals(paymentStatus.getStatus(), event.getType());} +} diff --git a/src/main/java/com/zufar/onlinestore/security/configuration/SpringSecurityConfiguration.java b/src/main/java/com/zufar/onlinestore/security/configuration/SpringSecurityConfiguration.java index 0cc5365c..d48d885a 100644 --- a/src/main/java/com/zufar/onlinestore/security/configuration/SpringSecurityConfiguration.java +++ b/src/main/java/com/zufar/onlinestore/security/configuration/SpringSecurityConfiguration.java @@ -26,6 +26,7 @@ public class SpringSecurityConfiguration { private static final String API_AUTH_URL_PREFIX = "/api/auth/**"; private static final String API_DOCS_URL_PREFIX = "/api/docs/**"; public static final String ACTUATOR_ENDPOINTS_URL_PREFIX = "/actuator/**"; + public static final String WEBHOOK_PAYMENT_EVENT_ENDPOINT = "/api/v1/payment/event"; @Bean public SecurityFilterChain securityFilterChain(final HttpSecurity httpSecurity, @@ -34,6 +35,7 @@ public SecurityFilterChain securityFilterChain(final HttpSecurity httpSecurity, .csrf().disable() .authorizeHttpRequests() .requestMatchers(API_AUTH_URL_PREFIX).permitAll() + .requestMatchers(WEBHOOK_PAYMENT_EVENT_ENDPOINT).permitAll() .requestMatchers(API_DOCS_URL_PREFIX).permitAll() .requestMatchers(ACTUATOR_ENDPOINTS_URL_PREFIX).permitAll() .anyRequest().authenticated() 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 5d03f302..8924342b 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,6 +1,7 @@ CREATE TABLE IF NOT EXISTS payment ( payment_id BIGSERIAL PRIMARY KEY, + payment_intent_id VARCHAR(32) NOT NULL UNIQUE, currency VARCHAR(3) NOT NULL, items_total_price DECIMAL NOT NULL CHECK (items_total_price > 0), status VARCHAR(32),