Add payment domain and data access modules

This commit is contained in:
Ali CANLI
2022-07-13 12:49:02 +03:00
parent 2071a3dd86
commit ef9c18bfbd
66 changed files with 1551 additions and 49 deletions

3
.gitignore vendored
View File

@@ -30,4 +30,5 @@ build/
!**/src/test/**/build/
### VS Code ###
.vscode/
.vscode/
/infrastructure/docker-compose/volumes/

View File

@@ -0,0 +1,10 @@
package com.food.order.sysyem;
public class DomainConstants {
private DomainConstants() {
}
public static final String UTC = "UTC";
}

View File

@@ -4,5 +4,6 @@ package com.food.order.sysyem.event;
// Base Domain Event Generic Class
public interface DomainEvent<T> {
void fire();
}

View File

@@ -28,7 +28,7 @@ public class Money {
this.amount.compareTo(other.amount) > 0;
}
public Money substract(Money other) {
public Money subtract(Money other) {
return new Money(setScale(this.amount.subtract(other.getAmount())));
}

View File

@@ -0,0 +1,6 @@
package com.food.order.sysyem.valueobject;
public enum PaymentOrderStatus {
PENDING,
CANCELLED
}

View File

@@ -1,3 +1,3 @@
KAFKA_VERSION=7.0.1
GLOBAL_NETWORK=food-order-system
GROUP_ID=com.food-order-system
GROUP_ID=com.food.order

View File

@@ -9,16 +9,16 @@ services:
kafka-topics --bootstrap-server kafka-broker-1:9092 --list
echo -e 'Deleting kafka topics'
kafka-topics --bootstrap-server kafka-broker-1:9092 --topic payment-request --delete --if-exists
kafka-topics --bootstrap-server kafka-broker-1:9092 --topic payment-response --delete --if-exists
kafka-topics --bootstrap-server kafka-broker-1:9092 --topic restaurant-approval-request --delete --if-exists
kafka-topics --bootstrap-server kafka-broker-1:9092 --topic restaurant-approval-response --delete --if-exists
kafka-topics --bootstrap-server kafka-broker-1:9092 --topic payment-request-value --delete --if-exists
kafka-topics --bootstrap-server kafka-broker-1:9092 --topic payment-response-value --delete --if-exists
kafka-topics --bootstrap-server kafka-broker-1:9092 --topic restaurant-approval-request-value --delete --if-exists
kafka-topics --bootstrap-server kafka-broker-1:9092 --topic restaurant-approval-response-value --delete --if-exists
echo -e 'Creating kafka topics'
kafka-topics --bootstrap-server kafka-broker-1:9092 --create --if-not-exists --topic payment-request --replication-factor 3 --partitions 3
kafka-topics --bootstrap-server kafka-broker-1:9092 --create --if-not-exists --topic payment-response --replication-factor 3 --partitions 3
kafka-topics --bootstrap-server kafka-broker-1:9092 --create --if-not-exists --topic restaurant-approval-request --replication-factor 3 --partitions 3
kafka-topics --bootstrap-server kafka-broker-1:9092 --create --if-not-exists --topic restaurant-approval-response --replication-factor 3 --partitions 3
kafka-topics --bootstrap-server kafka-broker-1:9092 --create --if-not-exists --topic payment-request-value --replication-factor 3 --partitions 3
kafka-topics --bootstrap-server kafka-broker-1:9092 --create --if-not-exists --topic payment-response-value --replication-factor 3 --partitions 3
kafka-topics --bootstrap-server kafka-broker-1:9092 --create --if-not-exists --topic restaurant-approval-request-value --replication-factor 3 --partitions 3
kafka-topics --bootstrap-server kafka-broker-1:9092 --create --if-not-exists --topic restaurant-approval-response-value --replication-factor 3 --partitions 3
echo -e 'Successfully created the following topics:'

View File

@@ -1,48 +1,52 @@
package com.food.order.system.kafka.producer.service.impl;
import com.food.order.system.kafka.producer.exception.KafkaProducerException;
import com.food.order.system.kafka.producer.service.KafkaProducer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.avro.specific.SpecificRecordBase;
import org.springframework.kafka.KafkaException;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.stereotype.Component;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
import javax.annotation.PreDestroy;
import java.io.Serializable;
import java.util.Objects;
@Slf4j
@Component
@RequiredArgsConstructor
@Slf4j
public class KafkaProducerImpl<K extends Serializable, V extends SpecificRecordBase> implements KafkaProducer<K, V> {
private final KafkaTemplate<K, V> kafkaTemplate;
public KafkaProducerImpl(KafkaTemplate<K, V> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
@Override
public void send(String topicName, K key, V message, ListenableFutureCallback<SendResult<K, V>> callback) {
log.info("Sending message={} to topic={}", message, topicName);
try {
ListenableFuture<SendResult<K, V>> kafkaResultFuture = kafkaTemplate.send(topicName, key, message);
kafkaResultFuture.addCallback(callback);
} catch (KafkaException e) {
log.error("Error on kafka producer with key: {}, message: {} and exception: {}", key, message,
e.getMessage());
throw new KafkaProducerException("Error on kafka producer with key: " + key + " and message: " + message);
}
log.info("Sending message to topic: {}, also message {}", topicName,message);
kafkaTemplate.send(topicName, key, message)
.addCallback(new ListenableFutureCallback<>() {
@Override
public void onFailure(Throwable ex) {
log.error("Error sending message to topic: {}, also message {}", topicName, message, ex);
callback.onFailure(ex);
}
@Override
public void onSuccess(SendResult<K, V> result) {
log.info("Message sent to topic: {}, also message {}", topicName, message);
callback.onSuccess(result);
}
});
}
@PreDestroy
public void close() {
if (kafkaTemplate != null) {
log.info("Closing kafka producer!");
public void destroy() {
log.info("KafkaProducerImpl is being destroyed");
if (Objects.nonNull(kafkaTemplate)) {
kafkaTemplate.destroy();
}
}
}

View File

@@ -5,10 +5,10 @@ logging:
com.food.order.system : DEBUG
order-service:
payment-request-topic-name: payment-request
payment-response-topic-name: payment-response
restaurant-approval-request-topic-name: restaurant-approval-request
restaurant-approval-response-topic-name: restaurant-approval-response
payment-request-topic-name: payment-request-value
payment-response-topic-name: payment-response-value
restaurant-approval-request-topic-name: restaurant-approval-request-value
restaurant-approval-response-topic-name: restaurant-approval-response-value
spring:
jpa:

View File

@@ -1,6 +1,7 @@
package com.food.order.sysyem;
import com.food.order.sysyem.dto.create.CreateOrderCommand;
import com.food.order.sysyem.event.publisher.DomainEventPublisher;
import com.food.order.sysyem.mapper.OrderDataMapper;
import com.food.order.sysyem.ports.output.repository.CustomerRepository;
import com.food.order.sysyem.ports.output.repository.OrderRepository;
@@ -27,6 +28,7 @@ public class OrderCreateHelper {
private final CustomerRepository customerRepository;
private final RestaurantRepository restaurantRepository;
private final OrderDataMapper orderDataMapper;
private final DomainEventPublisher<OrderCreatedEvent> publisher;
@Transactional
public OrderCreatedEvent persistOrder(CreateOrderCommand createOrderCommand) {
@@ -34,7 +36,7 @@ public class OrderCreateHelper {
checkCustomer(createOrderCommand.customerId());
Restaurant restaurant = checkRestaurant(createOrderCommand);
var order = orderDataMapper.createOrderCommandToOrder(createOrderCommand);
var createdEventOrder = orderDomainService.validateAndInitiateOrder(order, restaurant);
var createdEventOrder = orderDomainService.validateAndInitiateOrder(order, restaurant,publisher);
saveOrder(order);
log.info("Created Order Event : {}", createdEventOrder);
return createdEventOrder;

View File

@@ -1,12 +1,22 @@
package com.food.order.system.domain.event;
import com.food.order.system.domain.entity.Order;
import com.food.order.sysyem.event.publisher.DomainEventPublisher;
import java.time.ZonedDateTime;
public class OrderCancelledEvent extends OrderEvent {
public OrderCancelledEvent(Order order, ZonedDateTime utc) {
private final DomainEventPublisher<OrderCancelledEvent> publisher;
public OrderCancelledEvent(Order order, ZonedDateTime utc, DomainEventPublisher<OrderCancelledEvent> publisher) {
super(order, utc);
this.publisher = publisher;
}
@Override
public void fire() {
publisher.publish(this);
}
}

View File

@@ -1,11 +1,20 @@
package com.food.order.system.domain.event;
import com.food.order.system.domain.entity.Order;
import com.food.order.sysyem.event.publisher.DomainEventPublisher;
import java.time.ZonedDateTime;
public class OrderCreatedEvent extends OrderEvent {
public OrderCreatedEvent(Order order, ZonedDateTime createdAt) {
private final DomainEventPublisher<OrderCreatedEvent> publisher;
public OrderCreatedEvent(Order order, ZonedDateTime createdAt, DomainEventPublisher<OrderCreatedEvent> publisher) {
super(order, createdAt);
this.publisher = publisher;
}
@Override
public void fire() {
publisher.publish(this);
}
}

View File

@@ -1,13 +1,21 @@
package com.food.order.system.domain.event;
import com.food.order.system.domain.entity.Order;
import com.food.order.sysyem.event.publisher.DomainEventPublisher;
import java.time.ZonedDateTime;
public class OrderPaidEvent extends OrderEvent {
public OrderPaidEvent(Order order, ZonedDateTime utc) {
private final DomainEventPublisher<OrderPaidEvent> publisher;
public OrderPaidEvent(Order order, ZonedDateTime utc, DomainEventPublisher<OrderPaidEvent> publisher) {
super(order, utc);
this.publisher = publisher;
}
@Override
public void fire() {
publisher.publish(this);
}
}

View File

@@ -5,18 +5,19 @@ import com.food.order.system.domain.entity.Restaurant;
import com.food.order.system.domain.event.OrderCancelledEvent;
import com.food.order.system.domain.event.OrderCreatedEvent;
import com.food.order.system.domain.event.OrderPaidEvent;
import com.food.order.sysyem.event.publisher.DomainEventPublisher;
import java.util.List;
public interface OrderDomainService {
OrderCreatedEvent validateAndInitiateOrder(Order order, Restaurant restaurant);
OrderCreatedEvent validateAndInitiateOrder(Order order, Restaurant restaurant, DomainEventPublisher<OrderCreatedEvent> publisher);
OrderPaidEvent payOrder(Order order);
OrderPaidEvent payOrder(Order order,DomainEventPublisher<OrderPaidEvent> publisher);
void approve(Order order);
OrderCancelledEvent cancelOrderPayment(Order order, List<String> failureMessages);
OrderCancelledEvent cancelOrderPayment(Order order, List<String> failureMessages,DomainEventPublisher<OrderCancelledEvent> publisher);
void cancelOrder(Order order, List<String> failureMessages);

View File

@@ -7,6 +7,7 @@ import com.food.order.system.domain.event.OrderCreatedEvent;
import com.food.order.system.domain.event.OrderPaidEvent;
import com.food.order.system.domain.exception.OrderDomainException;
import com.food.order.system.domain.service.OrderDomainService;
import com.food.order.sysyem.event.publisher.DomainEventPublisher;
import lombok.extern.slf4j.Slf4j;
import java.time.ZoneId;
@@ -19,13 +20,14 @@ public class OrderDomainServiceImpl implements OrderDomainService {
private static final String UTC = "UTC";
@Override
public OrderCreatedEvent validateAndInitiateOrder(Order order, Restaurant restaurant) {
public OrderCreatedEvent validateAndInitiateOrder(Order order, Restaurant restaurant,
DomainEventPublisher<OrderCreatedEvent> publisher) {
validateRestaurant(restaurant);
setOrderProductInformation(order,restaurant);
order.validateOrder();
order.initializeOrder();
log.info("Order with id {} initialize successfully", order.getId().getValue());
return new OrderCreatedEvent(order, ZonedDateTime.now(ZoneId.of(UTC)));
return new OrderCreatedEvent(order, ZonedDateTime.now(ZoneId.of(UTC)),publisher);
}
private void setOrderProductInformation(Order order, Restaurant restaurant) {
@@ -46,10 +48,10 @@ public class OrderDomainServiceImpl implements OrderDomainService {
}
@Override
public OrderPaidEvent payOrder(Order order) {
public OrderPaidEvent payOrder(Order order, DomainEventPublisher<OrderPaidEvent> publisher) {
order.pay();
log.info("Order with id {} paid successfully", order.getId().getValue());
return new OrderPaidEvent(order, ZonedDateTime.now(ZoneId.of(UTC)));
return new OrderPaidEvent(order, ZonedDateTime.now(ZoneId.of(UTC)),publisher);
}
@Override
@@ -59,10 +61,11 @@ public class OrderDomainServiceImpl implements OrderDomainService {
}
@Override
public OrderCancelledEvent cancelOrderPayment(Order order, List<String> failureMessages) {
public OrderCancelledEvent cancelOrderPayment(Order order, List<String> failureMessages,
DomainEventPublisher<OrderCancelledEvent> publisher) {
order.initCancel(failureMessages);
log.info("Order with id {} cancelled successfully", order.getId().getValue());
return new OrderCancelledEvent(order, ZonedDateTime.now(ZoneId.of(UTC)));
return new OrderCancelledEvent(order, ZonedDateTime.now(ZoneId.of(UTC)),publisher);
}
@Override

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>payment-service</artifactId>
<groupId>com.food.order</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>payment-container</artifactId>
</project>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>payment-service</artifactId>
<groupId>com.food.order</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>payment-dataaccess</artifactId>
<dependencies>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>payment-application-service</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,38 @@
package com.food.order.system.payment.data.access.creditentry.adapter;
import com.food.order.system.payment.application.service.ports.output.repository.CreditEntryRepository;
import com.food.order.system.payment.data.access.creditentry.mapper.CreditEntryDataAccessMapper;
import com.food.order.system.payment.data.access.creditentry.repository.CreditEntryJpaRepository;
import com.food.order.system.payment.service.domain.entity.CreditEntry;
import com.food.order.sysyem.valueobject.CustomerId;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class CreditEntryRepositoryImpl implements CreditEntryRepository {
private final CreditEntryJpaRepository creditEntryJpaRepository;
private final CreditEntryDataAccessMapper creditEntryDataAccessMapper;
public CreditEntryRepositoryImpl(CreditEntryJpaRepository creditEntryJpaRepository,
CreditEntryDataAccessMapper creditEntryDataAccessMapper) {
this.creditEntryJpaRepository = creditEntryJpaRepository;
this.creditEntryDataAccessMapper = creditEntryDataAccessMapper;
}
@Override
public CreditEntry save(CreditEntry creditEntry) {
return creditEntryDataAccessMapper
.creditEntryEntityToCreditEntry(creditEntryJpaRepository
.save(creditEntryDataAccessMapper.creditEntryToCreditEntryEntity(creditEntry)));
}
@Override
public Optional<CreditEntry> findByCustomerId(CustomerId customerId) {
return creditEntryJpaRepository
.findByCustomerId(customerId.getValue())
.map(creditEntryDataAccessMapper::creditEntryEntityToCreditEntry);
}
}

View File

@@ -0,0 +1,38 @@
package com.food.order.system.payment.data.access.creditentry.entity;
import lombok.*;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.math.BigDecimal;
import java.util.Objects;
import java.util.UUID;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "credit_entry")
@Entity
public class CreditEntryEntity {
@Id
private UUID id;
private UUID customerId;
private BigDecimal totalCreditAmount;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CreditEntryEntity that = (CreditEntryEntity) o;
return id.equals(that.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}

View File

@@ -0,0 +1,8 @@
package com.food.order.system.payment.data.access.creditentry.exception;
public class CreditEntryDataaccessException extends RuntimeException {
public CreditEntryDataaccessException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,30 @@
package com.food.order.system.payment.data.access.creditentry.mapper;
import com.food.order.system.payment.data.access.creditentry.entity.CreditEntryEntity;
import com.food.order.system.payment.service.domain.entity.CreditEntry;
import com.food.order.system.payment.service.domain.valueobject.CreditEntryId;
import com.food.order.sysyem.valueobject.CustomerId;
import com.food.order.sysyem.valueobject.Money;
import org.springframework.stereotype.Component;
@Component
public class CreditEntryDataAccessMapper {
public CreditEntry creditEntryEntityToCreditEntry(CreditEntryEntity creditEntryEntity) {
return CreditEntry.builder()
.id(new CreditEntryId(creditEntryEntity.getId()))
.customerId(new CustomerId(creditEntryEntity.getCustomerId()))
.totalCreditAmount(new Money(creditEntryEntity.getTotalCreditAmount()))
.build();
}
public CreditEntryEntity creditEntryToCreditEntryEntity(CreditEntry creditEntry) {
return CreditEntryEntity.builder()
.id(creditEntry.getId().getValue())
.customerId(creditEntry.getCustomerId().getValue())
.totalCreditAmount(creditEntry.getTotalCreditAmount().getAmount())
.build();
}
}

View File

@@ -0,0 +1,16 @@
package com.food.order.system.payment.data.access.creditentry.repository;
import com.food.order.system.payment.data.access.creditentry.entity.CreditEntryEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface CreditEntryJpaRepository extends JpaRepository<CreditEntryEntity, UUID> {
Optional<CreditEntryEntity> findByCustomerId(UUID customerId);
}

View File

@@ -0,0 +1,44 @@
package com.food.order.system.payment.data.access.credithistory.adapter;
import com.food.order.system.payment.application.service.ports.output.repository.CreditHistoryRepository;
import com.food.order.system.payment.data.access.credithistory.entity.CreditHistoryEntity;
import com.food.order.system.payment.data.access.credithistory.mapper.CreditHistoryDataAccessMapper;
import com.food.order.system.payment.data.access.credithistory.repository.CreditHistoryJpaRepository;
import com.food.order.system.payment.service.domain.entity.CreditHistory;
import com.food.order.sysyem.valueobject.CustomerId;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Component
public class CreditHistoryRepositoryImpl implements CreditHistoryRepository {
private final CreditHistoryJpaRepository creditHistoryJpaRepository;
private final CreditHistoryDataAccessMapper creditHistoryDataAccessMapper;
public CreditHistoryRepositoryImpl(CreditHistoryJpaRepository creditHistoryJpaRepository,
CreditHistoryDataAccessMapper creditHistoryDataAccessMapper) {
this.creditHistoryJpaRepository = creditHistoryJpaRepository;
this.creditHistoryDataAccessMapper = creditHistoryDataAccessMapper;
}
@Override
public CreditHistory save(CreditHistory creditHistory) {
return creditHistoryDataAccessMapper.creditHistoryEntityToCreditHistory(creditHistoryJpaRepository
.save(creditHistoryDataAccessMapper.creditHistoryToCreditHistoryEntity(creditHistory)));
}
@Override
public Optional<List<CreditHistory>> findByCustomerId(CustomerId customerId) {
Optional<List<CreditHistoryEntity>> creditHistory =
creditHistoryJpaRepository.findByCustomerId(customerId.getValue());
return creditHistory
.map(creditHistoryList ->
creditHistoryList.stream()
.map(creditHistoryDataAccessMapper::creditHistoryEntityToCreditHistory)
.collect(Collectors.toList()));
}
}

View File

@@ -0,0 +1,39 @@
package com.food.order.system.payment.data.access.credithistory.entity;
import com.food.order.system.payment.service.domain.valueobject.TransactionType;
import lombok.*;
import javax.persistence.*;
import java.math.BigDecimal;
import java.util.Objects;
import java.util.UUID;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "credit_history")
@Entity
public class CreditHistoryEntity {
@Id
private UUID id;
private UUID customerId;
private BigDecimal amount;
@Enumerated(EnumType.STRING)
private TransactionType type;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CreditHistoryEntity that = (CreditHistoryEntity) o;
return id.equals(that.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}

View File

@@ -0,0 +1,8 @@
package com.food.order.system.payment.data.access.credithistory.exception;
public class CreditHistoryDataaccessException extends RuntimeException {
public CreditHistoryDataaccessException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,32 @@
package com.food.order.system.payment.data.access.credithistory.mapper;
import com.food.order.system.payment.data.access.credithistory.entity.CreditHistoryEntity;
import com.food.order.system.payment.service.domain.entity.CreditHistory;
import com.food.order.system.payment.service.domain.valueobject.CreditHistoryId;
import com.food.order.sysyem.valueobject.CustomerId;
import com.food.order.sysyem.valueobject.Money;
import org.springframework.stereotype.Component;
@Component
public class CreditHistoryDataAccessMapper {
public CreditHistory creditHistoryEntityToCreditHistory(CreditHistoryEntity creditHistoryEntity) {
return CreditHistory.builder()
.id(new CreditHistoryId(creditHistoryEntity.getId()))
.customerId(new CustomerId(creditHistoryEntity.getCustomerId()))
.amount(new Money(creditHistoryEntity.getAmount()))
.transactionType(creditHistoryEntity.getType())
.build();
}
public CreditHistoryEntity creditHistoryToCreditHistoryEntity(CreditHistory creditHistory) {
return CreditHistoryEntity.builder()
.id(creditHistory.getId().getValue())
.customerId(creditHistory.getCustomerId().getValue())
.amount(creditHistory.getAmount().getAmount())
.type(creditHistory.getTransactionType())
.build();
}
}

View File

@@ -0,0 +1,17 @@
package com.food.order.system.payment.data.access.credithistory.repository;
import com.food.order.system.payment.data.access.credithistory.entity.CreditHistoryEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface CreditHistoryJpaRepository extends JpaRepository<CreditHistoryEntity, UUID> {
Optional<List<CreditHistoryEntity>> findByCustomerId(UUID customerId);
}

View File

@@ -0,0 +1,37 @@
package com.food.order.system.payment.data.access.payment.adapter;
import com.food.order.system.payment.application.service.ports.output.repository.PaymentRepository;
import com.food.order.system.payment.data.access.payment.mapper.PaymentDataAccessMapper;
import com.food.order.system.payment.data.access.payment.repository.PaymentJpaRepository;
import com.food.order.system.payment.service.domain.entity.Payment;
import org.springframework.stereotype.Component;
import java.util.Optional;
import java.util.UUID;
@Component
public class PaymentRepositoryImpl implements PaymentRepository {
private final PaymentJpaRepository paymentJpaRepository;
private final PaymentDataAccessMapper paymentDataAccessMapper;
public PaymentRepositoryImpl(PaymentJpaRepository paymentJpaRepository,
PaymentDataAccessMapper paymentDataAccessMapper) {
this.paymentJpaRepository = paymentJpaRepository;
this.paymentDataAccessMapper = paymentDataAccessMapper;
}
@Override
public Payment save(Payment payment) {
return paymentDataAccessMapper
.paymentEntityToPayment(paymentJpaRepository
.save(paymentDataAccessMapper.paymentToPaymentEntity(payment)));
}
@Override
public Optional<Payment> findByOrderId(UUID orderId) {
return paymentJpaRepository.findByOrderId(orderId)
.map(paymentDataAccessMapper::paymentEntityToPayment);
}
}

View File

@@ -0,0 +1,42 @@
package com.food.order.system.payment.data.access.payment.entity;
import com.food.order.sysyem.valueobject.PaymentStatus;
import lombok.*;
import javax.persistence.*;
import java.math.BigDecimal;
import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.UUID;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "payments")
@Entity
public class PaymentEntity {
@Id
private UUID id;
private UUID customerId;
private UUID orderId;
private BigDecimal price;
@Enumerated(EnumType.STRING)
private PaymentStatus status;
private ZonedDateTime createdAt;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PaymentEntity that = (PaymentEntity) o;
return id.equals(that.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}

View File

@@ -0,0 +1,8 @@
package com.food.order.system.payment.data.access.payment.exception;
public class PaymentDataaccessException extends RuntimeException {
public PaymentDataaccessException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,36 @@
package com.food.order.system.payment.data.access.payment.mapper;
import com.food.order.system.payment.data.access.payment.entity.PaymentEntity;
import com.food.order.system.payment.service.domain.entity.Payment;
import com.food.order.system.payment.service.domain.valueobject.PaymentId;
import com.food.order.sysyem.valueobject.CustomerId;
import com.food.order.sysyem.valueobject.Money;
import com.food.order.sysyem.valueobject.OrderId;
import org.springframework.stereotype.Component;
@Component
public class PaymentDataAccessMapper {
public PaymentEntity paymentToPaymentEntity(Payment payment) {
return PaymentEntity.builder()
.id(payment.getId().getValue())
.customerId(payment.getCustomerId().getValue())
.orderId(payment.getOrderId().getValue())
.price(payment.getPrice().getAmount())
.status(payment.getStatus())
.createdAt(payment.getCreatedAt())
.build();
}
public Payment paymentEntityToPayment(PaymentEntity paymentEntity) {
return Payment.builder()
.id(new PaymentId(paymentEntity.getId()))
.customerId(new CustomerId(paymentEntity.getCustomerId()))
.orderId(new OrderId(paymentEntity.getOrderId()))
.price(new Money(paymentEntity.getPrice()))
.createdAt(paymentEntity.getCreatedAt())
.build();
}
}

View File

@@ -0,0 +1,16 @@
package com.food.order.system.payment.data.access.payment.repository;
import com.food.order.system.payment.data.access.payment.entity.PaymentEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface PaymentJpaRepository extends JpaRepository<PaymentEntity, UUID> {
Optional<PaymentEntity> findByOrderId(UUID orderId);
}

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>payment-domain</artifactId>
<groupId>com.food.order</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>payment-application-service</artifactId>
<dependencies>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>payment-domain</artifactId>
</dependency>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>common-domain</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>payment-domain-core</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,90 @@
package com.food.order.system.payment.application.service;
import com.food.order.system.payment.application.service.dto.PaymentRequest;
import com.food.order.system.payment.application.service.exception.PaymentApplicationServiceException;
import com.food.order.system.payment.application.service.mapper.PaymentDataMapper;
import com.food.order.system.payment.application.service.ports.output.repository.CreditEntryRepository;
import com.food.order.system.payment.application.service.ports.output.repository.CreditHistoryRepository;
import com.food.order.system.payment.application.service.ports.output.repository.PaymentRepository;
import com.food.order.system.payment.service.domain.PaymentDomainService;
import com.food.order.system.payment.service.domain.entity.CreditEntry;
import com.food.order.system.payment.service.domain.entity.CreditHistory;
import com.food.order.system.payment.service.domain.entity.Payment;
import com.food.order.system.payment.service.domain.event.PaymentCancelledEvent;
import com.food.order.system.payment.service.domain.event.PaymentCompletedEvent;
import com.food.order.system.payment.service.domain.event.PaymentEvent;
import com.food.order.system.payment.service.domain.event.PaymentFailedEvent;
import com.food.order.sysyem.event.publisher.DomainEventPublisher;
import com.food.order.sysyem.valueobject.CustomerId;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Component
@Slf4j
@RequiredArgsConstructor
public class PaymentRequestHelper {
private final PaymentDomainService paymentDomainService;
private final PaymentDataMapper paymentDataMapper;
private final PaymentRepository paymentRepository;
private final CreditEntryRepository creditEntryRepository;
private final CreditHistoryRepository creditHistoryRepository;
private final DomainEventPublisher<PaymentCompletedEvent> publisher;
private final DomainEventPublisher<PaymentCancelledEvent> publisherCancelled;
private final DomainEventPublisher<PaymentFailedEvent> failedEventDomainEventPublisher;
@Transactional
public PaymentEvent persistPayment(PaymentRequest paymentRequest) {
log.info("Received payment complete event for id : {}", paymentRequest.getOrderId());
var payment = paymentDataMapper.paymentRequestModelToPayment(paymentRequest);
var creditEntry = getCreditEntry(payment.getCustomerId());
var creditHistory = getCreditHistory(payment.getCustomerId());
List<String> failureMessage = new ArrayList<>();
var paymentEvent = paymentDomainService.validateAndInitializePayment
(payment, creditEntry, creditHistory, failureMessage,publisher,failedEventDomainEventPublisher);
persistDbObject(payment, creditEntry, creditHistory, failureMessage);
return paymentEvent;
}
public PaymentEvent persistCancelPayment(PaymentRequest paymentRequest) {
log.info("Received payment cancel event for id : {}", paymentRequest.getOrderId());
var payment = paymentRepository.findByOrderId
(UUID.fromString(paymentRequest.getOrderId())).orElseThrow(
() -> new PaymentApplicationServiceException("Payment not found"));
var creditEntry = getCreditEntry(payment.getCustomerId());
var creditHistory = getCreditHistory(payment.getCustomerId());
List<String> failureMessage = new ArrayList<>();
var paymentEvent = paymentDomainService.validateAndCancelledPayment
(payment, creditEntry, creditHistory, failureMessage,publisherCancelled,failedEventDomainEventPublisher);
persistDbObject(payment, creditEntry, creditHistory, failureMessage);
return paymentEvent;
}
private void persistDbObject(Payment payment, CreditEntry creditEntry, List<CreditHistory> creditHistory, List<String> failureMessage) {
paymentRepository.save(payment);
if (failureMessage.isEmpty()) {
creditEntryRepository.save(creditEntry);
creditHistoryRepository.save(creditHistory.get(creditHistory.size() - 1));
}
}
private List<CreditHistory> getCreditHistory(CustomerId customerId) {
return creditHistoryRepository.findByCustomerId(customerId).orElseThrow(
() -> new PaymentApplicationServiceException("No credit history found for customer id : " + customerId));
}
private CreditEntry getCreditEntry(CustomerId customerId) {
return creditEntryRepository.findByCustomerId(customerId).orElseThrow(
() -> new PaymentApplicationServiceException("Credit entry not found for customer id : " + customerId));
}
}

View File

@@ -0,0 +1,35 @@
package com.food.order.system.payment.application.service;
import com.food.order.system.payment.application.service.dto.PaymentRequest;
import com.food.order.system.payment.application.service.ports.input.message.listener.PaymentRequestMessageListener;
import com.food.order.system.payment.service.domain.event.PaymentEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
@RequiredArgsConstructor
public class PaymentRequestMessageListenerImpl implements PaymentRequestMessageListener {
private final PaymentRequestHelper paymentRequestHelper;
@Override
public void completePayment(PaymentRequest paymentRequest) {
var event = paymentRequestHelper.persistPayment(paymentRequest);
fireEvent(event);
}
@Override
public void cancelPayment(PaymentRequest paymentRequest) {
var event = paymentRequestHelper.persistCancelPayment(paymentRequest);
fireEvent(event);
}
private void fireEvent(PaymentEvent event) {
log.info("Firing event : {}", event);
event.fire();
}
}

View File

@@ -0,0 +1,15 @@
package com.food.order.system.payment.application.service.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "payment-service")
public class PaymentServiceConfigData {
private String paymentRequestTopicName;
private String paymentResponseTopicName;
}

View File

@@ -0,0 +1,26 @@
package com.food.order.system.payment.application.service.dto;
import com.food.order.sysyem.valueobject.PaymentOrderStatus;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import java.math.BigDecimal;
import java.time.Instant;
@Getter
@Builder
@AllArgsConstructor
public class PaymentRequest {
private String id;
private String sagaId;
private String orderId;
private String customerId;
private BigDecimal price;
private Instant createdAt;
private PaymentOrderStatus status;
public void setStatus(PaymentOrderStatus status) {
this.status = status;
}
}

View File

@@ -0,0 +1,14 @@
package com.food.order.system.payment.application.service.exception;
import com.food.order.sysyem.exception.DomainException;
public class PaymentApplicationServiceException extends DomainException {
public PaymentApplicationServiceException(String message) {
super(message);
}
public PaymentApplicationServiceException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,23 @@
package com.food.order.system.payment.application.service.mapper;
import com.food.order.system.payment.application.service.dto.PaymentRequest;
import com.food.order.system.payment.service.domain.entity.Payment;
import com.food.order.sysyem.valueobject.CustomerId;
import com.food.order.sysyem.valueobject.Money;
import com.food.order.sysyem.valueobject.OrderId;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Component
public class PaymentDataMapper {
public Payment paymentRequestModelToPayment(PaymentRequest paymentRequest) {
return Payment.builder()
.customerId(new CustomerId(UUID.fromString(paymentRequest.getCustomerId())))
.orderId(new OrderId(UUID.fromString(paymentRequest.getOrderId())))
.price(new Money(paymentRequest.getPrice()))
.build();
}
}

View File

@@ -0,0 +1,8 @@
package com.food.order.system.payment.application.service.ports.input.message.listener;
import com.food.order.system.payment.application.service.dto.PaymentRequest;
public interface PaymentRequestMessageListener {
void completePayment(PaymentRequest paymentRequest);
void cancelPayment(PaymentRequest paymentRequest);
}

View File

@@ -0,0 +1,7 @@
package com.food.order.system.payment.application.service.ports.output.message.publisher;
import com.food.order.system.payment.service.domain.event.PaymentCancelledEvent;
import com.food.order.sysyem.event.publisher.DomainEventPublisher;
public interface PaymentCancelledMessagePublisher extends DomainEventPublisher<PaymentCancelledEvent> {
}

View File

@@ -0,0 +1,7 @@
package com.food.order.system.payment.application.service.ports.output.message.publisher;
import com.food.order.system.payment.service.domain.event.PaymentCompletedEvent;
import com.food.order.sysyem.event.publisher.DomainEventPublisher;
public interface PaymentCompletedMessagePublisher extends DomainEventPublisher<PaymentCompletedEvent> {
}

View File

@@ -0,0 +1,7 @@
package com.food.order.system.payment.application.service.ports.output.message.publisher;
import com.food.order.system.payment.service.domain.event.PaymentFailedEvent;
import com.food.order.sysyem.event.publisher.DomainEventPublisher;
public interface PaymentFailedMessagePublisher extends DomainEventPublisher<PaymentFailedEvent> {
}

View File

@@ -0,0 +1,11 @@
package com.food.order.system.payment.application.service.ports.output.repository;
import com.food.order.system.payment.service.domain.entity.CreditEntry;
import com.food.order.sysyem.valueobject.CustomerId;
import java.util.Optional;
public interface CreditEntryRepository {
CreditEntry save(CreditEntry creditEntry);
Optional<CreditEntry> findByCustomerId(CustomerId orderId);
}

View File

@@ -0,0 +1,12 @@
package com.food.order.system.payment.application.service.ports.output.repository;
import com.food.order.system.payment.service.domain.entity.CreditHistory;
import com.food.order.sysyem.valueobject.CustomerId;
import java.util.List;
import java.util.Optional;
public interface CreditHistoryRepository {
CreditHistory save(CreditHistory creditHistory);
Optional<List<CreditHistory>> findByCustomerId(CustomerId orderId);
}

View File

@@ -0,0 +1,13 @@
package com.food.order.system.payment.application.service.ports.output.repository;
import com.food.order.system.payment.service.domain.entity.Payment;
import java.util.Optional;
import java.util.UUID;
public interface PaymentRepository {
Payment save(Payment payment);
Optional<Payment> findByOrderId(UUID orderId);
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>payment-domain</artifactId>
<groupId>com.food.order</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>payment-domain-core</artifactId>
<dependencies>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>common-domain</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,29 @@
package com.food.order.system.payment.service.domain;
import com.food.order.system.payment.service.domain.entity.CreditEntry;
import com.food.order.system.payment.service.domain.entity.CreditHistory;
import com.food.order.system.payment.service.domain.entity.Payment;
import com.food.order.system.payment.service.domain.event.PaymentCancelledEvent;
import com.food.order.system.payment.service.domain.event.PaymentCompletedEvent;
import com.food.order.system.payment.service.domain.event.PaymentEvent;
import com.food.order.system.payment.service.domain.event.PaymentFailedEvent;
import com.food.order.sysyem.event.publisher.DomainEventPublisher;
import java.util.List;
public interface PaymentDomainService {
PaymentEvent validateAndInitializePayment( Payment paymentEvent,
CreditEntry creditEntry,
List<CreditHistory> creditHistory,
List<String> failureMessages,
DomainEventPublisher<PaymentCompletedEvent> publisher,
DomainEventPublisher<PaymentFailedEvent> failedEventDomainEventPublisher);
PaymentEvent validateAndCancelledPayment( Payment paymentEvent,
CreditEntry creditEntry,
List<CreditHistory> creditHistory,
List<String> failureMessages,
DomainEventPublisher<PaymentCancelledEvent> publisher,
DomainEventPublisher<PaymentFailedEvent> failedEventDomainEventPublisher);
}

View File

@@ -0,0 +1,127 @@
package com.food.order.system.payment.service.domain;
import com.food.order.system.payment.service.domain.entity.CreditEntry;
import com.food.order.system.payment.service.domain.entity.CreditHistory;
import com.food.order.system.payment.service.domain.entity.Payment;
import com.food.order.system.payment.service.domain.event.PaymentCancelledEvent;
import com.food.order.system.payment.service.domain.event.PaymentCompletedEvent;
import com.food.order.system.payment.service.domain.event.PaymentEvent;
import com.food.order.system.payment.service.domain.event.PaymentFailedEvent;
import com.food.order.system.payment.service.domain.valueobject.CreditHistoryId;
import com.food.order.system.payment.service.domain.valueobject.TransactionType;
import com.food.order.sysyem.event.publisher.DomainEventPublisher;
import com.food.order.sysyem.valueobject.Money;
import com.food.order.sysyem.valueobject.PaymentStatus;
import lombok.extern.slf4j.Slf4j;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.UUID;
import static com.food.order.sysyem.DomainConstants.UTC;
@Slf4j
public class PaymentDomainServiceImpl implements PaymentDomainService {
@Override
public PaymentEvent validateAndInitializePayment(Payment payment,
CreditEntry creditEntry,
List<CreditHistory> creditHistory,
List<String> failureMessages,
DomainEventPublisher<PaymentCompletedEvent> publisher,
DomainEventPublisher<PaymentFailedEvent> failedPublisher) {
payment.validatePayment(failureMessages);
payment.initializePayment();
validateCreditEntry(payment,creditEntry,failureMessages);
subtractCreditEntry(payment,creditEntry);
updateCreditHistory(payment,creditHistory, TransactionType.DEBIT);
validateCreditHistory(creditEntry,creditHistory,failureMessages);
if (failureMessages.isEmpty()) {
log.info("Payment is valid and initialized");
payment.updateStatus(PaymentStatus.COMPLETED);
return new PaymentCompletedEvent(payment, ZonedDateTime.now(ZoneId.of(UTC)),publisher );
} else {
log.info("Payment is invalid and not initialized");
payment.updateStatus(PaymentStatus.FAILED);
return new PaymentFailedEvent(payment, ZonedDateTime.now(ZoneId.of(UTC)), failureMessages,failedPublisher);
}
}
@Override
public PaymentEvent validateAndCancelledPayment(Payment payment,
CreditEntry creditEntry,
List<CreditHistory> creditHistory,
List<String> failureMessages,
DomainEventPublisher<PaymentCancelledEvent> publisher,
DomainEventPublisher<PaymentFailedEvent> failedPublisher) {
payment.validatePayment(failureMessages);
addCreditEntry(payment,creditEntry);
updateCreditHistory(payment,creditHistory, TransactionType.CREDIT);
if (failureMessages.isEmpty()) {
log.info("Payment is valid and cancelled");
payment.updateStatus(PaymentStatus.CANCELED);
return new PaymentCancelledEvent(payment, ZonedDateTime.now(ZoneId.of(UTC)),publisher);
} else {
log.info("Payment is invalid and not cancelled");
payment.updateStatus(PaymentStatus.FAILED);
return new PaymentFailedEvent(payment, ZonedDateTime.now(ZoneId.of(UTC)), failureMessages,failedPublisher);
}
}
private void addCreditEntry(Payment payment, CreditEntry creditEntry) {
creditEntry.addCreditAmount(payment.getPrice());
}
private void validateCreditHistory(CreditEntry creditEntry, List<CreditHistory> creditHistory, List<String> failureMessages) {
var totalCreditHistory = getTotalHistoryAmount(creditHistory, TransactionType.CREDIT);
var totalDebitHistory = getTotalHistoryAmount(creditHistory, TransactionType.DEBIT);
if (totalDebitHistory.isGreaterThan(totalCreditHistory)) {
failureMessages.add("Customer id " + creditEntry.getCustomerId() + " has insufficient credit");
log.error("Customer id {} has insufficient credit", creditEntry.getCustomerId());
}
if (!creditEntry.getTotalCreditAmount().equals(totalCreditHistory.subtract(totalDebitHistory))) {
failureMessages.add("Customer id " + creditEntry.getCustomerId() + " has total is not equal to credit history");
log.error("Customer id {} has total is not equal to credit history", creditEntry.getCustomerId());
}
}
private Money getTotalHistoryAmount(List<CreditHistory> creditHistory, TransactionType transactionType) {
return creditHistory.stream()
.filter(history -> transactionType.equals(history.getTransactionType()))
.map(CreditHistory::getAmount)
.reduce(Money.ZERO, Money::add);
}
private void updateCreditHistory(Payment payment, List<CreditHistory> creditHistory, TransactionType transactionType) {
creditHistory.add(
CreditHistory.builder()
.id(new CreditHistoryId(UUID.randomUUID()))
.customerId(payment.getCustomerId())
.amount(payment.getPrice())
.transactionType(transactionType)
.build()
);
}
private void subtractCreditEntry(Payment payment, CreditEntry creditEntry) {
creditEntry.subtractCreditAmount(payment.getPrice());
}
private void validateCreditEntry(Payment payment, CreditEntry creditEntry, List<String> failureMessages) {
if(payment.getPrice().isGreaterThan(creditEntry.getTotalCreditAmount())){
failureMessages.add("Customer id "+ payment.getCustomerId() + " , has insufficient credit amount" +
creditEntry.getTotalCreditAmount() + " to pay for order id " + payment.getOrderId());
log.error("Payment price is greater than credit");
}
}
}

View File

@@ -0,0 +1,70 @@
package com.food.order.system.payment.service.domain.entity;
import com.food.order.system.payment.service.domain.valueobject.CreditEntryId;
import com.food.order.sysyem.entity.BaseEntity;
import com.food.order.sysyem.valueobject.CustomerId;
import com.food.order.sysyem.valueobject.Money;
public class CreditEntry extends BaseEntity<CreditEntryId> {
private final CustomerId customerId;
private Money totalCreditAmount;
public void addCreditAmount(Money creditAmount) {
totalCreditAmount = totalCreditAmount.add(creditAmount);
}
public void subtractCreditAmount(Money creditAmount) {
totalCreditAmount = totalCreditAmount.subtract(creditAmount);
}
private CreditEntry(Builder builder) {
setId(builder.creditEntryId);
customerId = builder.customerId;
totalCreditAmount = builder.totalCreditAmount;
}
public static Builder builder() {
return new Builder();
}
public CustomerId getCustomerId() {
return customerId;
}
public Money getTotalCreditAmount() {
return totalCreditAmount;
}
public static final class Builder {
private CreditEntryId creditEntryId;
private CustomerId customerId;
private Money totalCreditAmount;
private Builder() {
}
public Builder id(CreditEntryId val) {
creditEntryId = val;
return this;
}
public Builder customerId(CustomerId val) {
customerId = val;
return this;
}
public Builder totalCreditAmount(Money val) {
totalCreditAmount = val;
return this;
}
public CreditEntry build() {
return new CreditEntry(this);
}
}
}

View File

@@ -0,0 +1,72 @@
package com.food.order.system.payment.service.domain.entity;
import com.food.order.system.payment.service.domain.valueobject.CreditHistoryId;
import com.food.order.system.payment.service.domain.valueobject.TransactionType;
import com.food.order.sysyem.entity.BaseEntity;
import com.food.order.sysyem.valueobject.CustomerId;
import com.food.order.sysyem.valueobject.Money;
public class CreditHistory extends BaseEntity<CreditHistoryId> {
private final CustomerId customerId;
private final Money amount;
private final TransactionType transactionType;
private CreditHistory(Builder builder) {
setId(builder.creditHistoryId);
customerId = builder.customerId;
amount = builder.amount;
transactionType = builder.transactionType;
}
public static Builder builder() {
return new Builder();
}
public CustomerId getCustomerId() {
return customerId;
}
public Money getAmount() {
return amount;
}
public TransactionType getTransactionType() {
return transactionType;
}
public static final class Builder {
private CreditHistoryId creditHistoryId;
private CustomerId customerId;
private Money amount;
private TransactionType transactionType;
private Builder() {
}
public Builder id(CreditHistoryId val) {
creditHistoryId = val;
return this;
}
public Builder customerId(CustomerId val) {
customerId = val;
return this;
}
public Builder amount(Money val) {
amount = val;
return this;
}
public Builder transactionType(TransactionType val) {
transactionType = val;
return this;
}
public CreditHistory build() {
return new CreditHistory(this);
}
}
}

View File

@@ -0,0 +1,119 @@
package com.food.order.system.payment.service.domain.entity;
import com.food.order.system.payment.service.domain.valueobject.PaymentId;
import com.food.order.sysyem.entity.AggregateRoot;
import com.food.order.sysyem.valueobject.CustomerId;
import com.food.order.sysyem.valueobject.Money;
import com.food.order.sysyem.valueobject.OrderId;
import com.food.order.sysyem.valueobject.PaymentStatus;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
public class Payment extends AggregateRoot<PaymentId> {
private final OrderId orderId;
private final CustomerId customerId;
private final Money price;
private PaymentStatus status;
private ZonedDateTime createdAt;
public void initializePayment(){
setId(new PaymentId(UUID.randomUUID()));
createdAt = ZonedDateTime.now(ZoneId.of("UTC"));
}
public void validatePayment(List<String > failureMessages){
if (Objects.isNull(price) || !price.isGreaterThanZero()){
failureMessages.add("Payment price must be greater than zero");
}
}
public void updateStatus ( PaymentStatus status ) {
this.status = status;
}
private Payment(Builder builder) {
setId(builder.paymentId);
orderId = builder.orderId;
customerId = builder.customerId;
price = builder.price;
status = builder.status;
createdAt = builder.createdAt;
}
public static Builder builder() {
return new Builder();
}
public OrderId getOrderId() {
return orderId;
}
public CustomerId getCustomerId() {
return customerId;
}
public Money getPrice() {
return price;
}
public PaymentStatus getStatus() {
return status;
}
public ZonedDateTime getCreatedAt() {
return createdAt;
}
public static final class Builder {
private PaymentId paymentId;
private OrderId orderId;
private CustomerId customerId;
private Money price;
private PaymentStatus status;
private ZonedDateTime createdAt;
private Builder() {
}
public Builder id(PaymentId val) {
paymentId = val;
return this;
}
public Builder orderId(OrderId val) {
orderId = val;
return this;
}
public Builder customerId(CustomerId val) {
customerId = val;
return this;
}
public Builder price(Money val) {
price = val;
return this;
}
public Builder status(PaymentStatus val) {
status = val;
return this;
}
public Builder createdAt(ZonedDateTime val) {
createdAt = val;
return this;
}
public Payment build() {
return new Payment(this);
}
}
}

View File

@@ -0,0 +1,23 @@
package com.food.order.system.payment.service.domain.event;
import com.food.order.system.payment.service.domain.entity.Payment;
import com.food.order.sysyem.event.publisher.DomainEventPublisher;
import java.time.ZonedDateTime;
import java.util.Collections;
public class PaymentCancelledEvent extends PaymentEvent{
private final DomainEventPublisher<PaymentCancelledEvent> publisher;
public PaymentCancelledEvent(Payment payment, ZonedDateTime createdAt, DomainEventPublisher<PaymentCancelledEvent> publisher) {
super(payment, createdAt, Collections.emptyList());
this.publisher = publisher;
}
@Override
public void fire() {
publisher.publish(this);
}
}

View File

@@ -0,0 +1,25 @@
package com.food.order.system.payment.service.domain.event;
import com.food.order.system.payment.service.domain.entity.Payment;
import com.food.order.sysyem.event.publisher.DomainEventPublisher;
import java.time.ZonedDateTime;
import java.util.Collections;
public class PaymentCompletedEvent extends PaymentEvent{
private final DomainEventPublisher<PaymentCompletedEvent> publisher;
public PaymentCompletedEvent(Payment payment,
ZonedDateTime createdAt,
DomainEventPublisher<PaymentCompletedEvent> publisher) {
super(payment, createdAt , Collections.emptyList());
this.publisher = publisher;
}
@Override
public void fire() {
publisher.publish(this);
}
}

View File

@@ -0,0 +1,31 @@
package com.food.order.system.payment.service.domain.event;
import com.food.order.system.payment.service.domain.entity.Payment;
import com.food.order.sysyem.event.DomainEvent;
import java.time.ZonedDateTime;
import java.util.List;
public abstract class PaymentEvent implements DomainEvent<Payment> {
private final Payment payment;
private final ZonedDateTime createdAt;
private final List<String> failureMessages;
public PaymentEvent(Payment payment, ZonedDateTime createdAt, List<String> failureMessages) {
this.payment = payment;
this.createdAt = createdAt;
this.failureMessages = failureMessages;
}
public Payment getPayment() {
return payment;
}
public ZonedDateTime getCreatedAt() {
return createdAt;
}
public List<String> getFailureMessages() {
return failureMessages;
}
}

View File

@@ -0,0 +1,23 @@
package com.food.order.system.payment.service.domain.event;
import com.food.order.system.payment.service.domain.entity.Payment;
import com.food.order.sysyem.event.publisher.DomainEventPublisher;
import java.time.ZonedDateTime;
import java.util.List;
public class PaymentFailedEvent extends PaymentEvent{
private final DomainEventPublisher<PaymentFailedEvent> publisher;
public PaymentFailedEvent(Payment payment, ZonedDateTime createdAt, List<String> failureMessages,
DomainEventPublisher<PaymentFailedEvent> publisher) {
super(payment, createdAt, failureMessages);
this.publisher = publisher;
}
@Override
public void fire() {
publisher.publish(this);
}
}

View File

@@ -0,0 +1,13 @@
package com.food.order.system.payment.service.domain.exception;
import com.food.order.sysyem.exception.DomainException;
public class PayemntDomainException extends DomainException {
public PayemntDomainException(String message) {
super(message);
}
public PayemntDomainException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,13 @@
package com.food.order.system.payment.service.domain.exception;
import com.food.order.sysyem.exception.DomainException;
public class PaymentNotFoundException extends DomainException {
public PaymentNotFoundException(String message) {
super(message);
}
public PaymentNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,11 @@
package com.food.order.system.payment.service.domain.valueobject;
import com.food.order.sysyem.valueobject.BaseId;
import java.util.UUID;
public class CreditEntryId extends BaseId<UUID> {
public CreditEntryId(UUID value) {
super(value);
}
}

View File

@@ -0,0 +1,12 @@
package com.food.order.system.payment.service.domain.valueobject;
import com.food.order.sysyem.valueobject.BaseId;
import java.util.UUID;
public class CreditHistoryId extends BaseId<UUID> {
public CreditHistoryId(UUID value) {
super(value);
}
}

View File

@@ -0,0 +1,11 @@
package com.food.order.system.payment.service.domain.valueobject;
import com.food.order.sysyem.valueobject.BaseId;
import java.util.UUID;
public class PaymentId extends BaseId<UUID> {
public PaymentId(UUID id) {
super(id);
}
}

View File

@@ -0,0 +1,5 @@
package com.food.order.system.payment.service.domain.valueobject;
public enum TransactionType {
DEBIT, CREDIT
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>payment-service</artifactId>
<groupId>com.food.order</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>payment-domain</artifactId>
<packaging>pom</packaging>
<modules>
<module>payment-domain-core</module>
<module>payment-application-service</module>
</modules>
</project>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>payment-service</artifactId>
<groupId>com.food.order</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>payment-messaging</artifactId>
</project>

22
payment-service/pom.xml Normal file
View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>food-ordering-system</artifactId>
<groupId>com.food.order</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>payment-service</artifactId>
<packaging>pom</packaging>
<modules>
<module>payment-domain</module>
<module>payment-dataaccess</module>
<module>payment-messaging</module>
<module>payment-container</module>
</modules>
</project>

15
pom.xml
View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.7.1</version>
<version>2.6.7</version>
<relativePath/>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -15,6 +15,7 @@
<module>infrastructure</module>
<module>infrastructure/kafka</module>
<module>customer-service</module>
<module>payment-service</module>
</modules>
@@ -41,12 +42,24 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>payment-domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>order-app</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>payment-application-service</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>order-application-service</artifactId>