Skip to content

Commit

Permalink
Merge pull request #4 from badass-techie/feature/create-payment-micro…
Browse files Browse the repository at this point in the history
…service

Create payment microservice
  • Loading branch information
badass-techie authored Jan 6, 2024
2 parents 841d9bf + 1c0929b commit c204c22
Show file tree
Hide file tree
Showing 34 changed files with 715 additions and 95 deletions.
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ DISCOVERY_SERVER_HOST=discovery-server
IDENTITY_DB_URL=jdbc:postgresql://identity-db:5432/identity
IDENTITY_DB_USERNAME=
IDENTITY_DB_PASSWORD=
MPESA_BUSINESS_SHORTCODE=
MPESA_CONSUMER_KEY=
MPESA_CONSUMER_SECRET=
MPESA_PASSKEY=
ORDER_DB_URL=jdbc:postgresql://order-db:5432/order
ORDER_DB_USERNAME=
ORDER_DB_PASSWORD=
PRODUCT_DB_URL=mongodb://product-db:27017/product
RABBITMQ_BASE=amqp://rabbitmq:5672
STRIPE_SECRET_KEY=
ZIPKIN_BASE=http://zipkin:9411
23 changes: 23 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,26 @@ COPY --from=0 /build/Product/target/product-1.0-SNAPSHOT.jar product.jar

# Run the jar file
ENTRYPOINT ["java","-jar","product.jar"]


# Payment
# Use lightweight python image as base image
FROM python:3.10-alpine AS payment

# make sure all messages always reach console
ENV PYTHONUNBUFFERED=1

# prevent writing bytecode
ENV PYTHONDONTWRITEBYTECODE=1

# Copy the entire python project
COPY Payment /Payment

# Set working directory
WORKDIR /Payment

# Install dependencies
RUN pip install -r requirements.txt

# Run the python file
ENTRYPOINT ["python", "app.py"]
16 changes: 16 additions & 0 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,19 @@ WORKDIR /app

# Build all maven modules
RUN mvn clean install -DskipTests


# This image is used to build the payment microservice
FROM python:3.10-slim AS payment

# make sure all messages always reach console
ENV PYTHONUNBUFFERED=1

# prevent writing bytecode
ENV PYTHONDONTWRITEBYTECODE=1

COPY Payment /app

WORKDIR /app

RUN pip install -r requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.badasstechie.order.config;

import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
Expand All @@ -14,15 +14,28 @@ public class RabbitMQConfig {
@Value("${message-bus.exchange-name}")
private String exchangeName;

@Value("${message-bus.queue-name}")
private String queueName;
@Value("${message-bus.queues.update-stock}")
private String updateStockQueueName;

@Value("${message-bus.queues.order-awaiting-payment}")
private String orderAwaitingPaymentQueueName;

@Value("${message-bus.routing-key}")
private String routingKey;
@Value("${message-bus.queues.order-payment-processed}")
private String orderPaymentProcessedQueueName;

@Bean
public Queue updateStockQueue() {
return new Queue(updateStockQueueName);
}

@Bean
public Queue queue() {
return new Queue(queueName);
public Queue orderAwaitingPaymentQueue() {
return new Queue(orderAwaitingPaymentQueueName);
}

@Bean
public Queue orderPaymentProcessedQueue() {
return new Queue(orderPaymentProcessedQueueName);
}

@Bean
Expand All @@ -31,22 +44,42 @@ public TopicExchange exchange() {
}

@Bean
public Binding binding(Queue queue, TopicExchange exchange) {
return BindingBuilder
.bind(queue)
.to(exchange)
.with(routingKey);
public Binding updateStockBinding(Queue updateStockQueue, TopicExchange exchange) {
return BindingBuilder.bind(updateStockQueue).to(exchange).with(updateStockQueueName);
}

@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
public Binding orderAwaitingPaymentBinding(Queue orderAwaitingPaymentQueue, TopicExchange exchange) {
return BindingBuilder.bind(orderAwaitingPaymentQueue).to(exchange).with(orderAwaitingPaymentQueueName);
}

@Bean
public Binding orderPaymentProcessedBinding(Queue orderPaymentProcessedQueue, TopicExchange exchange) {
return BindingBuilder.bind(orderPaymentProcessedQueue).to(exchange).with(orderPaymentProcessedQueueName);
}

@Bean
public AmqpTemplate template(ConnectionFactory connectionFactory) {
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}

@Bean AmqpTemplate updateStockTemplate(ConnectionFactory connectionFactory) {
return createTemplate(connectionFactory, updateStockQueueName);
}

@Bean AmqpTemplate orderAwaitingPaymentTemplate(ConnectionFactory connectionFactory) {
return createTemplate(connectionFactory, orderAwaitingPaymentQueueName);
}

@Bean AmqpTemplate orderPaymentProcessedTemplate(ConnectionFactory connectionFactory) {
return createTemplate(connectionFactory, orderPaymentProcessedQueueName);
}

private AmqpTemplate createTemplate(ConnectionFactory connectionFactory, String routingKey) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setExchange(exchangeName);
template.setRoutingKey(routingKey);
template.setMessageConverter(messageConverter());
return template;
return template;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.badasstechie.order.dto;

import java.math.BigDecimal;
import java.util.Map;

/**
* This is a record class that represents an order payment request to be sent to the payment microservice via the message bus and then to the payment gateway.
*
* @param orderId The order ID.
* @param orderNumber The order number.
* @param amount Total amount to be paid for in the smallest unit of the currency.
* @param currency The currency in which the payment is made.
* @param paymentMethod Exactly one of 'stripe', 'paypal', or 'mpesa'.
* @param payerDetails A map of payer details. The keys are the names of the fields required by the payment gateway such as 'cardNumber', 'cvc', 'expiryMonth', 'expiryYear', and 'phoneNumber'.
*/
public record OrderPaymentRequest(
Long orderId,
String orderNumber,
BigDecimal amount,
String currency,
String paymentMethod,
Map<String, String> payerDetails
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.badasstechie.order.dto;

import java.math.BigDecimal;
import java.util.Map;

public record OrderPaymentResponse(
Long orderId,
String orderNumber,
BigDecimal amount,
String currency,
String paymentMethod,
Map<String, String> payerDetails,
PaymentResult resultStatus,
String resultMessage
) {}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.badasstechie.order.dto;

import java.util.List;
import java.util.Map;

public record OrderRequest(
List<OrderItemDto> items,
String deliveryAddress
String deliveryAddress,
String paymentMethod,
Map<String, String> payerDetails
){}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.badasstechie.order.dto;

public enum PaymentResult {
SUCCESS,
FAILED
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.badasstechie.order.model;

public enum OrderStatus {
CREATED,
AWAITING_PAYMENT,
SHIPPING,
COMPLETED,
CANCELLED
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package com.badasstechie.order.service;

import com.badasstechie.order.dto.OrderItemDto;
import com.badasstechie.order.dto.OrderRequest;
import com.badasstechie.order.dto.OrderResponse;
import com.badasstechie.order.dto.ProductStockDto;
import com.badasstechie.order.dto.*;
import com.badasstechie.order.model.Order;
import com.badasstechie.order.model.OrderItem;
import com.badasstechie.order.model.OrderStatus;
Expand All @@ -12,37 +9,37 @@
import com.badasstechie.product.grpc.ProductStocksRequest;
import com.badasstechie.product.grpc.ProductStocksResponse;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import lombok.extern.slf4j.Slf4j;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
import java.util.*;

@Service
@Slf4j
public class OrderService {
private final OrderRepository orderRepository;
private final RabbitTemplate rabbitTemplate;
private final AmqpTemplate updateStockTemplate, orderAwaitingPaymentTemplate;

@GrpcClient("product-grpc-service")
private ProductGrpcServiceGrpc.ProductGrpcServiceBlockingStub productGrpcService;

@Value("${message-bus.exchange-name}")
private String messageBusExchangeName;

@Value("${message-bus.routing-key}")
private String messageBusRoutingKey;
private final Map<String, List<String>> paymentRequirements = new HashMap<>();

@Autowired
public OrderService(OrderRepository orderRepository, RabbitTemplate rabbitTemplate) {
public OrderService(OrderRepository orderRepository, AmqpTemplate updateStockTemplate, AmqpTemplate orderAwaitingPaymentTemplate) {
this.orderRepository = orderRepository;
this.rabbitTemplate = rabbitTemplate;
this.updateStockTemplate = updateStockTemplate;
this.orderAwaitingPaymentTemplate = orderAwaitingPaymentTemplate;
paymentRequirements.put("stripe", List.of("cardToken"));
paymentRequirements.put("mpesa", List.of("phoneNumber"));
}

private OrderResponse mapOrderToResponse(Order order) {
Expand Down Expand Up @@ -82,6 +79,18 @@ private OrderItemDto mapOrderItemToDto(OrderItem orderItem) {
}

public ResponseEntity<OrderResponse> placeOrder(OrderRequest orderRequest, Long userId) {
// validate the payment details
String paymentMethod = orderRequest.paymentMethod();
if (!paymentRequirements.containsKey(paymentMethod)) {
throw new RuntimeException("Unknown payment method");
}

List<String> requiredFields = paymentRequirements.get(paymentMethod);
for (String field : requiredFields) {
if (!orderRequest.payerDetails().containsKey(field))
throw new RuntimeException(field + " not found but required for " + paymentMethod + " payment");
}

List<ProductStockDto> stocks = getProductStocks(orderRequest.items().stream().map(OrderItemDto::productId).toList());

// throw exception if length of stocks and order items are not equal
Expand All @@ -100,18 +109,32 @@ public ResponseEntity<OrderResponse> placeOrder(OrderRequest orderRequest, Long
.userId(userId)
.orderNumber(UUID.randomUUID().toString())
.items(orderRequest.items().stream().map(this::mapDtoToOrderItem).toList())
.status(OrderStatus.CREATED)
.status(OrderStatus.AWAITING_PAYMENT)
.deliveryAddress(orderRequest.deliveryAddress())
.created(Instant.now())
.build()
);

// publish message to message bus with the product ids and quantities
// publish message to message bus for payment to be processed
OrderPaymentRequest paymentRequest = new OrderPaymentRequest(
order.getId(),
order.getOrderNumber(),
order.getItems().stream().reduce(
BigDecimal.ZERO,
(subtotal, orderItem) -> subtotal.add(orderItem.getUnitPrice().multiply(BigDecimal.valueOf(orderItem.getQuantity()))),
BigDecimal::add
),
"KES",
orderRequest.paymentMethod(),
orderRequest.payerDetails()
);
orderAwaitingPaymentTemplate.convertAndSend(paymentRequest);

// publish message to message bus for stock to be updated
List<ProductStockDto> productsOrdered = order.getItems().stream()
.map(item -> new ProductStockDto(item.getProductId(), item.getQuantity()))
.toList();

rabbitTemplate.convertAndSend(messageBusExchangeName, messageBusRoutingKey, productsOrdered);
updateStockTemplate.convertAndSend(productsOrdered);

return new ResponseEntity<>(mapOrderToResponse(order), HttpStatus.CREATED);
}
Expand All @@ -127,6 +150,23 @@ public List<ProductStockDto> getProductStocks(List<String> ids) {
.toList();
}

// update order status after payment has been processed
@RabbitListener(queues = "${message-bus.queues.order-payment-processed}")
public void updateOrderStatus(OrderPaymentResponse paymentResponse) {
Order order = orderRepository.findById(paymentResponse.orderId())
.orElseThrow(() -> new RuntimeException("Order not found"));

if (paymentResponse.resultStatus() == PaymentResult.SUCCESS) {
order.setStatus(OrderStatus.SHIPPING);
orderRepository.save(order);
} else if (paymentResponse.resultStatus() == PaymentResult.FAILED) {
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
}

log.info("Payment processed for order number {}. Result: {}", paymentResponse.orderNumber(), paymentResponse.resultMessage());
}

public OrderResponse getOrder(Long id) {
return orderRepository.findById(id)
.map(this::mapOrderToResponse)
Expand Down
8 changes: 5 additions & 3 deletions Order/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,11 @@ resilience4j:

# custom properties for our message bus
message-bus:
exchange-name: 'eshoponsteroids' # name of the exchange
queue-name: 'new-order' # name of the queue
routing-key: 'new-order' # used to route messages from the exchange to the specified queue
exchange-name: 'eshoponsteroids'
queues:
update-stock: 'update-stock'
order-awaiting-payment: 'order-awaiting-payment'
order-payment-processed: 'order-payment-processed'

springdoc:
api-docs:
Expand Down
Loading

0 comments on commit c204c22

Please sign in to comment.