Skip to content

Commit

Permalink
first payment tests
Browse files Browse the repository at this point in the history
  • Loading branch information
benjaminpochat committed Jun 10, 2024
1 parent 3458ccf commit 3d0d029
Show file tree
Hide file tree
Showing 19 changed files with 652 additions and 272 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package eu.viandeendirect.domains.order;

import eu.viandeendirect.api.OrdersApiDelegate;
import eu.viandeendirect.domains.payment.StripePaymentRepository;
import eu.viandeendirect.domains.payment.StripeService;
import eu.viandeendirect.domains.user.CustomerRepository;
import eu.viandeendirect.model.*;
Expand All @@ -16,6 +17,7 @@
import java.util.ArrayList;
import java.util.List;

import static org.springframework.http.HttpStatus.CREATED;
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;

@Service
Expand All @@ -33,17 +35,56 @@ public class OrderService implements OrdersApiDelegate {

@Autowired
private StripeService stripeService;

@Autowired
private CustomerRepository customerRepository;

@Autowired
private StripePaymentRepository stripePaymentRepository;

@Override
public ResponseEntity<Order> getOrder(Integer orderId) {
Order order = orderRepository.findById(orderId).get();
List<OrderItem> items = orderItemRepository.findByOrder(order);
order.setItems(items);
return new ResponseEntity<>(order, HttpStatus.OK);
}

@Override
public ResponseEntity<Order> createOrder(Order order) {
loadCustomer(order);
order.setStatus(OrderStatus.ITEMS_SELECTED);
Order orderCreated = orderRepository.save(order);
updateQuantitiesSold(order);
return new ResponseEntity<>(orderCreated, CREATED);
}

@Override
public ResponseEntity<Order> createOrderPayment(Order order) {
try {
loadCustomer(order);
order.setStatus(OrderStatus.ITEMS_SELECTED);
orderRepository.save(order);
updateQuantitiesSold(order);
StripePayment payment = stripeService.createPayment(order);
stripePaymentRepository.save(payment);
order.setPayment(payment);
orderRepository.save(order);
return new ResponseEntity<>(order, CREATED);
} catch (Exception e) {
LOGGER.error("An error occurred when creating Stripe payment using Stripe API", e);
throw new ResponseStatusException(INTERNAL_SERVER_ERROR, "Une erreur s'est produite à la création du paiement Stripe", e);
}
}

private void loadCustomer(Order order) {
if (order.getCustomer().getId() == null) {
Customer customer = customerRepository.findByEmail(order.getCustomer().getUser().getEmail()).orElse(null);
order.setCustomer(customer);
}
order.setStatus(OrderStatus.ITEMS_SELECTED);
Order orderCreated = orderRepository.save(order);
}

private void updateQuantitiesSold(Order order) {
List<PackageLot> lots = new ArrayList<>();
order.getItems().forEach(item -> {
PackageLot lot = packageLotRepository.findById(item.getPackageLot().getId()).get();
Expand All @@ -57,54 +98,28 @@ public ResponseEntity<Order> createOrder(Order order) {
%s articles ont été mis en vente.
Le nombre total d'articles vendus ne peut pas dépasser la quantité totals du lot.
""",
item.getQuantity(),
lot.getId(),
lot.getLabel(),
lot.getQuantitySold(),
lot.getQuantity()));
item.getQuantity(),
lot.getId(),
lot.getLabel(),
lot.getQuantitySold(),
lot.getQuantity()));
}
lot.setQuantitySold(updatedQuantySold);
lots.add(lot);
});
packageLotRepository.saveAll(lots);
orderItemRepository.saveAll(order.getItems());
return new ResponseEntity<>(orderCreated, HttpStatus.CREATED);
}

@Override
public ResponseEntity<Order> getOrder(Integer orderId) {
Order order = orderRepository.findById(orderId).get();
List<OrderItem> items = orderItemRepository.findByOrder(order);
order.setItems(items);
return new ResponseEntity<>(order, HttpStatus.OK);
}

@Override
public ResponseEntity<StripePayment> createOrderPayment(Integer orderId) {
Order order = orderRepository.findById(orderId).get();
try {
StripePayment payment = stripeService.createPayment(order);
return new ResponseEntity<>(payment, HttpStatus.OK);
} catch (Exception e) {
LOGGER.error("An error occurred when creating Stripe account data using Stripe API", e);
throw new ResponseStatusException(INTERNAL_SERVER_ERROR, "Une erreur s'est produite à la création du compte Stripe", e);
}
}

public void processOrderPaymentInitialization(Order order) {
order.setStatus(OrderStatus.PAYMENT_STARTED);
orderRepository.save(order);
}

public void processOrderPaymentValidation(Order order) {
stripeService.transferPaymentToProducers(order);
order.setStatus(OrderStatus.PAYED);
public void processOrderPaymentCompletion(Order order) {
order.setStatus(OrderStatus.PAYMENT_COMPLETED);
// TODO: trigger an email to the customer
orderRepository.save(order);
}

public void processOrderPaymentFailure(Order order) {
public void processOrderPaymentExpiration(Order order) {
order.setStatus(OrderStatus.PAYMENT_FAILED);
// TODO : update quantity to sold with product not paid
orderRepository.save(order);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package eu.viandeendirect.domains.payment;

import com.stripe.exception.StripeException;
import com.stripe.model.checkout.Session;
import com.stripe.net.RequestOptions;
import com.stripe.param.checkout.SessionCreateParams;
import eu.viandeendirect.domains.production.PackageLotRepository;
import eu.viandeendirect.model.Order;
import eu.viandeendirect.model.OrderItem;
import eu.viandeendirect.model.Producer;
import eu.viandeendirect.model.StripePayment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.time.temporal.TemporalUnit;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
* Manager Stripe payments following the Stripe pattern "direct charge".
* To use when orders includes productions from the same producer.
*
* @see <a href="https://docs.stripe.com/connect/direct-charges">Stripe doc</a>
*/
@Service
@Qualifier("StripeDirectPaymentManager")
public class StripeDirectPaymentManager implements StripePaymentManager {

private static final Logger LOGGER = LoggerFactory.getLogger(StripeDirectPaymentManager.class);

@Value("${PRODUCER_FRONTEND_URL:http://localhost:3000}")
String viandeendirectProducerFrontendUrl;

@Autowired
PackageLotRepository packageLotRepository;

@Override
public StripePayment createPayment(Order order) throws StripeException {
SessionCreateParams.Builder builder = SessionCreateParams.builder();
order.getItems().forEach(item -> builder.addLineItem(getLineItem(item)));
SessionCreateParams params = builder
.setPaymentIntentData(SessionCreateParams.PaymentIntentData.builder()
.setDescription(String.format("Commande viandeendirect.eu n° %s de %s %s", order.getId(), order.getCustomer().getUser().getFirstName(), order.getCustomer().getUser().getLastName()))
.setApplicationFeeAmount(1L).build())
.setMode(SessionCreateParams.Mode.PAYMENT)
.setCustomerEmail(order.getCustomer().getUser().getEmail())
.setSuccessUrl(viandeendirectProducerFrontendUrl + "/order/" + order.getId() + "/paymentSuccessful")
.setCancelUrl(viandeendirectProducerFrontendUrl + "/order/" + order.getId() + "/paymentCancelled")
.setExpiresAt(Instant.now().plusSeconds(30 * 60).getEpochSecond())
.build();
RequestOptions requestOptions = RequestOptions.builder().setStripeAccount(getProducerStripeAccount(order).getStripeAccount().getStripeId()).build();
Session session = Session.create(params, requestOptions);
StripePayment stripePayment = new StripePayment();
stripePayment.setCheckoutSessionId(session.getId());
stripePayment.setPaymentUrl(session.getUrl());
return stripePayment;
}

private Producer getProducerStripeAccount(Order order) {
List<Producer> orderProducers = order.getItems().stream()
.map(OrderItem::getPackageLot)
.map(lot -> packageLotRepository.findById(lot.getId()).get())
.map(lot -> lot.getProduction().getProducer())
.distinct()
.toList();
if (orderProducers.size() > 1) {
throw new IllegalStateException("StripeDirectPaymentManager can only be used with orders from the same producer");
}
return orderProducers.stream().findFirst().get();
}

@Override
public void processPaymentValidation(Order order) {
LOGGER.debug("processing payment validation for order {} : do nothing while payment manager is {}", order.getId(), this.getClass().getSimpleName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,21 @@
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

import static org.springframework.http.HttpStatus.*;
import static org.springframework.http.HttpStatus.NOT_IMPLEMENTED;

/**
* @link <a href=https://docs.stripe.com/connect/onboarding/quickstart#init-stripe>Stripe documentation</a>
*/
@RestController
public class StripeEventHandler {

@Value("${STRIPE_WEBHOOK_SECRET:default_stripe_webhook_secret_value}")
private String stripeWebhookSecret;
@Value("${STRIPE_WEBHOOK_ACCOUNT_SECRET:default_stripe_webhook_secret_value}")
private String stripeWebhookAccountSecret;

@Value("${STRIPE_WEBHOOK_CONNECT_SECRET:default_stripe_webhook_secret_value}")
private String stripeWebhookConnectSecret;


private static final Logger LOGGER = LoggerFactory.getLogger(StripeEventHandler.class);

Expand All @@ -36,50 +43,68 @@ public class StripeEventHandler {
private OrderService orderService;


@PostMapping(value = "/payments/stripeEvents", produces = "application/json")
public ResponseEntity<String> handleStripeEvent(@RequestBody String stripeEvent, @RequestHeader("Stripe-Signature") String stripeSignature) {
@PostMapping(value = "/payments/stripeAccountEvents", produces = "application/json")
public ResponseEntity<String> handleStripeAccountEvent(@RequestBody String stripeEvent, @RequestHeader("Stripe-Signature") String stripeSignature) {
try {
Event event = Webhook.constructEvent(stripeEvent, stripeSignature, stripeWebhookSecret);
Event event = Webhook.constructEvent(stripeEvent, stripeSignature, stripeWebhookAccountSecret);
switch (event.getType()) {
case "checkout.session.completed":
LOGGER.info("Stripe event handled : Checkout session completed");
processOrderPaymentInitialization(event);
LOGGER.info("Stripe account event handled : Checkout session completed");
break;
case "checkout.session.expired":
LOGGER.info("Stripe account event handled : Checkout session expired");
break;
case "checkout.session.async_payment_succeeded":
LOGGER.info("Stripe event handled : Checkout session async payment succeeded");
processOrderPaymentValidation(event);
LOGGER.info("Stripe account event handled : Checkout session async payment succeeded");
break;
case "checkout.session.async_payment_failed":
LOGGER.warn("Stripe event handled : Checkout session async payment failed");
processOrderPaymentFailure(event);
LOGGER.warn("Stripe account event handled : Checkout session async payment failed");
break;
default:
LOGGER.error("Unhandled event type: {}", event.getType());
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
LOGGER.info("Unhandled account event type: {}", event.getType());
return new ResponseEntity<>(NOT_IMPLEMENTED);
}
} catch (SignatureVerificationException e) {
LOGGER.error("An error occurred when processing Stripe webhook event", e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
LOGGER.error("An error occurred when processing Stripe webhook account event", e);
return new ResponseEntity<>(INTERNAL_SERVER_ERROR);
}
return new ResponseEntity<>(HttpStatus.OK);
return new ResponseEntity<>(OK);
}

private void processOrderPaymentInitialization(Event event) {
Session checkoutSession = (Session) event.getData().getObject();
Order order = findOrderByCheckoutSession(checkoutSession);
orderService.processOrderPaymentInitialization(order);
@PostMapping(value = "/payments/stripeConnectEvents", produces = "application/json")
public ResponseEntity<String> handleStripeConnectEvent(@RequestBody String stripeEvent, @RequestHeader("Stripe-Signature") String stripeSignature) {
try {
Event event = Webhook.constructEvent(stripeEvent, stripeSignature, stripeWebhookConnectSecret);
switch (event.getType()) {
case "checkout.session.completed":
LOGGER.info("Stripe connect event handled : Checkout session completed");
processOrderPaymentCompleted(event);
break;
case "checkout.session.expired":
LOGGER.info("Stripe connect event handled : Checkout session expired");
processOrderPaymentExpiration(event);
break;
default:
LOGGER.info("Unhandled connect event type: {}", event.getType());
return new ResponseEntity<>(NOT_IMPLEMENTED);
}
} catch (SignatureVerificationException e) {
LOGGER.error("An error occurred when processing Stripe webhook connect event", e);
return new ResponseEntity<>(INTERNAL_SERVER_ERROR);
}
return new ResponseEntity<>(OK);
}

private void processOrderPaymentValidation(Event event) {
private void processOrderPaymentCompleted(Event event) {
Session checkoutSession = (Session) event.getData().getObject();
Order order = findOrderByCheckoutSession(checkoutSession);
orderService.processOrderPaymentValidation(order);
orderService.processOrderPaymentCompletion(order);
}

private void processOrderPaymentFailure(Event event) {
private void processOrderPaymentExpiration(Event event) {
Session checkoutSession = (Session) event.getData().getObject();
Order order = findOrderByCheckoutSession(checkoutSession);
orderService.processOrderPaymentFailure(order);
orderService.processOrderPaymentExpiration(order);
}

private Order findOrderByCheckoutSession(Session checkoutSession) {
Expand Down
Loading

0 comments on commit 3d0d029

Please sign in to comment.