Merge pull request #16 from alicanli1995/cqrs-pattern

Cqrs pattern
This commit is contained in:
Ali CANLI
2022-07-17 15:03:51 +03:00
committed by GitHub
58 changed files with 1657 additions and 103 deletions

38
.idea/compiler.xml generated
View File

@@ -11,8 +11,27 @@
<module name="order-application-service" />
<module name="common-domain" />
<module name="order-core-domain" />
<module name="customer-container" />
<module name="customer-service-application" />
<module name="restaurant-core-domain" />
<module name="payment-domain-core" />
<module name="saga" />
<module name="restaurant-application-service" />
<module name="customer-messaging" />
<module name="order-application-service" />
<module name="kafka-producer" />
<module name="customer-data-access" />
<module name="order-data-access" />
<module name="payment-dataaccess" />
<module name="common-data-access" />
<module name="outbox" />
<module name="order-container" />
<module name="order-app" />
<module name="restaurant-dataaccess" />
<module name="payment-container" />
<module name="customer-application" />
<module name="customer-domain-core" />
<module name="restaurant-messaging" />
</profile>
</annotationProcessing>
<bytecodeTargetLevel>
@@ -21,9 +40,26 @@
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="common" options="-parameters" />
<module name="clean-hexagonal-architecture-kafka-saga-outbox" options="" />
<module name="common" options="" />
<module name="common-application" options="-parameters" />
<module name="common-data-access" options="-parameters" />
<module name="common-domain" options="-parameters" />
<module name="customer-application" options="-parameters" />
<module name="customer-container" options="-parameters" />
<module name="customer-data-access" options="-parameters" />
<module name="customer-domain" options="" />
<module name="customer-domain-core" options="-parameters" />
<module name="customer-messaging" options="-parameters" />
<module name="customer-service" options="" />
<module name="customer-service-application" options="-parameters" />
<module name="food-ordering-system" options="" />
<module name="infrastructure" options="" />
<module name="kafka" options="" />
<module name="kafka-config" options="-parameters" />
<module name="kafka-consumer" options="-parameters" />
<module name="kafka-model" options="-parameters" />
<module name="kafka-producer" options="-parameters" />
<module name="order-app" options="-parameters" />
<module name="order-application-service" options="-parameters" />
<module name="order-container" options="-parameters" />

20
.idea/encodings.xml generated
View File

@@ -4,6 +4,26 @@
<file url="file://$PROJECT_DIR$/common/common-domain/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/common/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/common/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/customer-service/customer-application/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/customer-service/customer-container/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/customer-service/customer-data-access/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/customer-service/customer-domain/customer-domain-core/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/customer-service/customer-domain/customer-service-application/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/customer-service/customer-domain/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/customer-service/customer-domain/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/customer-service/customer-messaging/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/customer-service/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/customer-service/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/infrastructure/kafka/kafka-config/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/infrastructure/kafka/kafka-consumer/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/infrastructure/kafka/kafka-model/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/infrastructure/kafka/kafka-producer/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/infrastructure/kafka/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/infrastructure/kafka/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/infrastructure/outbox/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/infrastructure/saga/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/infrastructure/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/infrastructure/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/order-service/order-app/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/order-service/order-app/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/order-service/order-container/src/main/java" charset="UTF-8" />

View File

@@ -0,0 +1,33 @@
<?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>customer-service</artifactId>
<groupId>com.food.order</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>customer-application</artifactId>
<dependencies>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>customer-service-application</artifactId>
</dependency>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>common-application</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,26 @@
package com.food.order.system.customer.rest.application.handler;
import com.food.order.system.application.handler.ErrorDTO;
import com.food.order.system.application.handler.GlobalExceptionHandler;
import com.food.order.system.customer.domain.exception.CustomerDomainException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
@Slf4j
@ControllerAdvice
public class CustomerGlobalExceptionHandler extends GlobalExceptionHandler {
@ResponseBody
@ExceptionHandler(value = {CustomerDomainException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorDTO handleException(CustomerDomainException exception) {
log.error(exception.getMessage(), exception);
return ErrorDTO.builder().code(HttpStatus.BAD_REQUEST.getReasonPhrase())
.message(exception.getMessage()).build();
}
}

View File

@@ -0,0 +1,31 @@
package com.food.order.system.customer.rest.application.rest;
import com.food.order.system.customer.service.create.CreateCustomerCommand;
import com.food.order.system.customer.service.create.CreateCustomerResponse;
import com.food.order.system.customer.service.ports.input.service.CustomerApplicationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
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;
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping(value = "/customers", produces = "application/vnd.api.v1+json")
public class CustomerController {
private final CustomerApplicationService customerApplicationService;
@PostMapping
public ResponseEntity<CreateCustomerResponse> createCustomer(@RequestBody CreateCustomerCommand
createCustomerCommand) {
log.info("Creating customer with username: {}", createCustomerCommand.username());
CreateCustomerResponse response = customerApplicationService.createCustomer(createCustomerCommand);
return ResponseEntity.ok(response);
}
}

View File

@@ -0,0 +1,63 @@
<?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>customer-service</artifactId>
<groupId>com.food.order</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>customer-container</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>customer-service-application</artifactId>
</dependency>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>customer-domain-core</artifactId>
</dependency>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>customer-data-access</artifactId>
</dependency>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>customer-messaging</artifactId>
</dependency>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>customer-application</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<name>${project.groupId}/customer.service:${project.version}</name>
</image>
</configuration>
<executions>
<execution>
<phase>install</phase>
<goals>
<goal>build-image</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,15 @@
package com.food.order.system.customer.service;
import com.food.order.system.customer.domain.CustomerDomainService;
import com.food.order.system.customer.domain.CustomerDomainServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BeanConfiguration {
@Bean
public CustomerDomainService customerDomainService() {
return new CustomerDomainServiceImpl();
}
}

View File

@@ -0,0 +1,19 @@
package com.food.order.system.customer.service;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableJpaRepositories(basePackages = {"com.food.order.system.customer.dataaccess",
"com.food.order.system.common.data.access"})
@EntityScan(basePackages = {"com.food.order.system.customer.dataaccess",
"com.food.order.system.common.data.access"})
@SpringBootApplication(scanBasePackages = "com.food.order")
public class CustomerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CustomerServiceApplication.class, args);
}
}

View File

@@ -0,0 +1,43 @@
server:
port: 8184
customer-service:
customer-topic-name: customer
spring:
jpa:
open-in-view: false
show-sql: false
database-platform: org.hibernate.dialect.PostgreSQL9Dialect
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQL9Dialect
datasource:
url: jdbc:postgresql://localhost:5432/postgres?currentSchema=customer&binaryTransfer=true&reWriteBatchedInserts=true
username: postgres
password: postgres
driver-class-name: org.postgresql.Driver
sql:
init:
mode: always
schema-locations: classpath:init-schema.sql
platform: postgres
kafka-config:
bootstrap-servers: localhost:19092, localhost:29092, localhost:39092
schema-registry-url-key: schema.registry.url
schema-registry-url: http://localhost:8081
num-of-partitions: 3
replication-factor: 3
kafka-producer-config:
key-serializer-class: org.apache.kafka.common.serialization.StringSerializer
value-serializer-class: io.confluent.kafka.serializers.KafkaAvroSerializer
compression-type: snappy
acks: all
batch-size: 16384
batch-size-boost-factor: 100
linger-ms: 5
request-timeout-ms: 60000
retry-count: 5

View File

@@ -0,0 +1,33 @@
<?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>customer-service</artifactId>
<groupId>com.food.order</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>customer-data-access</artifactId>
<dependencies>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>customer-service-application</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>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>common-data-access</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,27 @@
package com.food.order.system.customer.dataaccess.adapter;
import com.food.order.system.customer.dataaccess.mapper.CustomerDataAccessMapper;
import com.food.order.system.customer.dataaccess.repository.CustomerJpaRepository;
import com.food.order.system.customer.domain.entity.Customer;
import com.food.order.system.customer.service.ports.output.repository.CustomerRepository;
import org.springframework.stereotype.Component;
@Component
public class CustomerRepositoryImpl implements CustomerRepository {
private final CustomerJpaRepository customerJpaRepository;
private final CustomerDataAccessMapper customerDataAccessMapper;
public CustomerRepositoryImpl(CustomerJpaRepository customerJpaRepository,
CustomerDataAccessMapper customerDataAccessMapper) {
this.customerJpaRepository = customerJpaRepository;
this.customerDataAccessMapper = customerDataAccessMapper;
}
@Override
public Customer createCustomer(Customer customer) {
return customerDataAccessMapper.customerEntityToCustomer(
customerJpaRepository.save(customerDataAccessMapper.customerToCustomerEntity(customer)));
}
}

View File

@@ -0,0 +1,24 @@
package com.food.order.system.customer.dataaccess.entity;
import lombok.*;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.UUID;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "customers")
@Entity
public class CustomerEntity {
@Id
private UUID id;
private String username;
private String firstName;
private String lastName;
}

View File

@@ -0,0 +1,8 @@
package com.food.order.system.customer.dataaccess.exception;
public class CustomerDataaccessException extends RuntimeException {
public CustomerDataaccessException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,27 @@
package com.food.order.system.customer.dataaccess.mapper;
import com.food.order.system.customer.dataaccess.entity.CustomerEntity;
import com.food.order.system.customer.domain.entity.Customer;
import com.food.order.system.valueobject.CustomerId;
import org.springframework.stereotype.Component;
@Component
public class CustomerDataAccessMapper {
public Customer customerEntityToCustomer(CustomerEntity customerEntity) {
return new Customer(new CustomerId(customerEntity.getId()),
customerEntity.getUsername(),
customerEntity.getFirstName(),
customerEntity.getLastName());
}
public CustomerEntity customerToCustomerEntity(Customer customer) {
return CustomerEntity.builder()
.id(customer.getId().getValue())
.username(customer.getUsername())
.firstName(customer.getFirstName())
.lastName(customer.getLastName())
.build();
}
}

View File

@@ -0,0 +1,13 @@
package com.food.order.system.customer.dataaccess.repository;
import com.food.order.system.customer.dataaccess.entity.CustomerEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.UUID;
@Repository
public interface CustomerJpaRepository extends JpaRepository<CustomerEntity, UUID> {
}

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>customer-domain</artifactId>
<groupId>com.food.order</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>customer-domain-core</artifactId>
<dependencies>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>common-domain</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,11 @@
package com.food.order.system.customer.domain;
import com.food.order.system.customer.domain.entity.Customer;
import com.food.order.system.customer.domain.event.CustomerCreatedEvent;
public interface CustomerDomainService {
CustomerCreatedEvent validateAndInitiateCustomer(Customer customer);
}

View File

@@ -0,0 +1,19 @@
package com.food.order.system.customer.domain;
import com.food.order.system.customer.domain.entity.Customer;
import com.food.order.system.customer.domain.event.CustomerCreatedEvent;
import lombok.extern.slf4j.Slf4j;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@Slf4j
public class CustomerDomainServiceImpl implements CustomerDomainService {
public CustomerCreatedEvent validateAndInitiateCustomer(Customer customer) {
//Any Business logic required to run for a customer creation
log.info("Customer with id: {} is initiated", customer.getId().getValue());
return new CustomerCreatedEvent(customer, ZonedDateTime.now(ZoneId.of("UTC")));
}
}

View File

@@ -0,0 +1,32 @@
package com.food.order.system.customer.domain.entity;
import com.food.order.system.entity.AggregateRoot;
import com.food.order.system.valueobject.CustomerId;
public class Customer extends AggregateRoot<CustomerId> {
private final String username;
private final String firstName;
private final String lastName;
public Customer(CustomerId customerId, String username, String firstName, String lastName) {
super.setId(customerId);
this.username = username;
this.firstName = firstName;
this.lastName = lastName;
}
public String getUsername() {
return username;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}

View File

@@ -0,0 +1,23 @@
package com.food.order.system.customer.domain.event;
import com.food.order.system.customer.domain.entity.Customer;
import com.food.order.system.event.DomainEvent;
import java.time.ZonedDateTime;
public class CustomerCreatedEvent implements DomainEvent<Customer> {
private final Customer customer;
private final ZonedDateTime createdAt;
public CustomerCreatedEvent(Customer customer, ZonedDateTime createdAt) {
this.customer = customer;
this.createdAt = createdAt;
}
public Customer getCustomer() {
return customer;
}
}

View File

@@ -0,0 +1,11 @@
package com.food.order.system.customer.domain.exception;
import com.food.order.system.exception.DomainException;
public class CustomerDomainException extends DomainException {
public CustomerDomainException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,35 @@
<?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>customer-domain</artifactId>
<groupId>com.food.order</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>customer-service-application</artifactId>
<dependencies>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>customer-domain-core</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>
</dependencies>
</project>

View File

@@ -0,0 +1,36 @@
package com.food.order.system.customer.service;
import com.food.order.system.customer.domain.event.CustomerCreatedEvent;
import com.food.order.system.customer.service.create.CreateCustomerCommand;
import com.food.order.system.customer.service.create.CreateCustomerResponse;
import com.food.order.system.customer.service.mapper.CustomerDataMapper;
import com.food.order.system.customer.service.ports.input.service.CustomerApplicationService;
import com.food.order.system.customer.service.ports.output.message.publisher.CustomerMessagePublisher;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@Slf4j
@Validated
@Service
@RequiredArgsConstructor
class CustomerApplicationServiceImpl implements CustomerApplicationService {
private final CustomerCreateCommandHandler customerCreateCommandHandler;
private final CustomerDataMapper customerDataMapper;
private final CustomerMessagePublisher customerMessagePublisher;
@Override
public CreateCustomerResponse createCustomer(CreateCustomerCommand createCustomerCommand) {
CustomerCreatedEvent customerCreatedEvent = customerCreateCommandHandler.createCustomer(createCustomerCommand);
customerMessagePublisher.publish(customerCreatedEvent);
return customerDataMapper
.customerToCreateCustomerResponse(customerCreatedEvent.getCustomer(),
"Customer saved successfully!");
}
}

View File

@@ -0,0 +1,42 @@
package com.food.order.system.customer.service;
import com.food.order.system.customer.domain.CustomerDomainService;
import com.food.order.system.customer.domain.entity.Customer;
import com.food.order.system.customer.domain.event.CustomerCreatedEvent;
import com.food.order.system.customer.domain.exception.CustomerDomainException;
import com.food.order.system.customer.service.create.CreateCustomerCommand;
import com.food.order.system.customer.service.mapper.CustomerDataMapper;
import com.food.order.system.customer.service.ports.output.repository.CustomerRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Component
@RequiredArgsConstructor
class CustomerCreateCommandHandler {
private final CustomerDomainService customerDomainService;
private final CustomerRepository customerRepository;
private final CustomerDataMapper customerDataMapper;
@Transactional
public CustomerCreatedEvent createCustomer(CreateCustomerCommand createCustomerCommand) {
Customer customer = customerDataMapper.createCustomerCommandToCustomer(createCustomerCommand);
CustomerCreatedEvent customerCreatedEvent = customerDomainService.validateAndInitiateCustomer(customer);
Customer savedCustomer = customerRepository.createCustomer(customer);
if (savedCustomer == null) {
log.error("Could not save customer with id: {}", createCustomerCommand.customerId());
throw new CustomerDomainException("Could not save customer with id " +
createCustomerCommand.customerId());
}
log.info("Returning CustomerCreatedEvent for customer id: {}", createCustomerCommand.customerId());
return customerCreatedEvent;
}
}

View File

@@ -0,0 +1,12 @@
package com.food.order.system.customer.service.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "customer-service")
public class CustomerServiceConfigData {
private String customerTopicName;
}

View File

@@ -0,0 +1,11 @@
package com.food.order.system.customer.service.create;
import lombok.Builder;
import javax.validation.constraints.NotNull;
import java.util.UUID;
@Builder
public record CreateCustomerCommand(@NotNull UUID customerId, @NotNull String username, @NotNull String firstName,
@NotNull String lastName) {
}

View File

@@ -0,0 +1,10 @@
package com.food.order.system.customer.service.create;
import lombok.Builder;
import javax.validation.constraints.NotNull;
import java.util.UUID;
@Builder
public record CreateCustomerResponse(@NotNull UUID customerId, @NotNull String message) {
}

View File

@@ -0,0 +1,22 @@
package com.food.order.system.customer.service.mapper;
import com.food.order.system.customer.domain.entity.Customer;
import com.food.order.system.customer.service.create.CreateCustomerCommand;
import com.food.order.system.customer.service.create.CreateCustomerResponse;
import com.food.order.system.valueobject.CustomerId;
import org.springframework.stereotype.Component;
@Component
public class CustomerDataMapper {
public Customer createCustomerCommandToCustomer(CreateCustomerCommand createCustomerCommand) {
return new Customer(new CustomerId(createCustomerCommand.customerId()),
createCustomerCommand.username(),
createCustomerCommand.firstName(),
createCustomerCommand.lastName());
}
public CreateCustomerResponse customerToCreateCustomerResponse(Customer customer, String message) {
return new CreateCustomerResponse(customer.getId().getValue(), message);
}
}

View File

@@ -0,0 +1,13 @@
package com.food.order.system.customer.service.ports.input.service;
import com.food.order.system.customer.service.create.CreateCustomerCommand;
import com.food.order.system.customer.service.create.CreateCustomerResponse;
import javax.validation.Valid;
public interface CustomerApplicationService {
CreateCustomerResponse createCustomer(@Valid CreateCustomerCommand createCustomerCommand);
}

View File

@@ -0,0 +1,10 @@
package com.food.order.system.customer.service.ports.output.message.publisher;
import com.food.order.system.customer.domain.event.CustomerCreatedEvent;
public interface CustomerMessagePublisher {
void publish(CustomerCreatedEvent customerCreatedEvent);
}

View File

@@ -0,0 +1,9 @@
package com.food.order.system.customer.service.ports.output.repository;
import com.food.order.system.customer.domain.entity.Customer;
public interface CustomerRepository {
Customer createCustomer(Customer customer);
}

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>customer-service</artifactId>
<groupId>com.food.order</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>customer-domain</artifactId>
<packaging>pom</packaging>
<modules>
<module>customer-domain-core</module>
<module>customer-service-application</module>
</modules>
</project>

View File

@@ -0,0 +1,30 @@
<?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>customer-service</artifactId>
<groupId>com.food.order</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>customer-messaging</artifactId>
<dependencies>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>customer-service-application</artifactId>
</dependency>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>kafka-producer</artifactId>
</dependency>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>kafka-model</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,19 @@
package com.food.order.system.customer.messaging.mapper;
import com.food.order.system.customer.domain.event.CustomerCreatedEvent;
import com.food.order.system.kafka.order.avro.model.CustomerAvroModel;
import org.springframework.stereotype.Component;
@Component
public class CustomerMessagingDataMapper {
public CustomerAvroModel paymentResponseAvroModelToPaymentResponse(CustomerCreatedEvent
customerCreatedEvent) {
return CustomerAvroModel.newBuilder()
.setId(customerCreatedEvent.getCustomer().getId().getValue().toString())
.setUsername(customerCreatedEvent.getCustomer().getUsername())
.setFirstName(customerCreatedEvent.getCustomer().getFirstName())
.setLastName(customerCreatedEvent.getCustomer().getLastName())
.build();
}
}

View File

@@ -0,0 +1,68 @@
package com.food.order.system.customer.messaging.publisher.kafka;
import com.food.order.system.customer.domain.event.CustomerCreatedEvent;
import com.food.order.system.customer.messaging.mapper.CustomerMessagingDataMapper;
import com.food.order.system.customer.service.config.CustomerServiceConfigData;
import com.food.order.system.customer.service.ports.output.message.publisher.CustomerMessagePublisher;
import com.food.order.system.kafka.order.avro.model.CustomerAvroModel;
import com.food.order.system.kafka.producer.service.KafkaProducer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.springframework.kafka.support.SendResult;
import org.springframework.stereotype.Component;
import org.springframework.util.concurrent.ListenableFutureCallback;
@Slf4j
@Component
@RequiredArgsConstructor
public class CustomerCreatedEventKafkaPublisher implements CustomerMessagePublisher {
private final CustomerMessagingDataMapper customerMessagingDataMapper;
private final KafkaProducer<String, CustomerAvroModel> kafkaProducer;
private final CustomerServiceConfigData customerServiceConfigData;
@Override
public void publish(CustomerCreatedEvent customerCreatedEvent) {
log.info("Received CustomerCreatedEvent for customer id: {}",
customerCreatedEvent.getCustomer().getId().getValue());
try {
CustomerAvroModel customerAvroModel = customerMessagingDataMapper
.paymentResponseAvroModelToPaymentResponse(customerCreatedEvent);
kafkaProducer.send(customerServiceConfigData.getCustomerTopicName(), customerAvroModel.getId(),
customerAvroModel,
getCallback(customerServiceConfigData.getCustomerTopicName(), customerAvroModel));
log.info("CustomerCreatedEvent sent to kafka for customer id: {}",
customerAvroModel.getId());
} catch (Exception e) {
log.error("Error while sending CustomerCreatedEvent to kafka for customer id: {}," +
" error: {}", customerCreatedEvent.getCustomer().getId().getValue(), e.getMessage());
}
}
private ListenableFutureCallback<SendResult<String, CustomerAvroModel>>
getCallback(String topicName, CustomerAvroModel message) {
return new ListenableFutureCallback<>() {
@Override
public void onFailure(Throwable throwable) {
log.error("Error while sending message {} to topic {}", message.toString(), topicName, throwable);
}
@Override
public void onSuccess(SendResult<String, CustomerAvroModel> result) {
RecordMetadata metadata = result.getRecordMetadata();
log.info("Received new metadata. Topic: {}; Partition {}; Offset {}; Timestamp {}, at time {}",
metadata.topic(),
metadata.partition(),
metadata.offset(),
metadata.timestamp(),
System.nanoTime());
}
};
}
}

View File

@@ -10,46 +10,15 @@
<modelVersion>4.0.0</modelVersion>
<artifactId>customer-service</artifactId>
<packaging>pom</packaging>
<modules>
<module>customer-container</module>
<module>customer-application</module>
<module>customer-data-access</module>
<module>customer-messaging</module>
<module>customer-domain</module>
</modules>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<name>${project.groupId}/customer.service:${project.version}</name>
</image>
</configuration>
<executions>
<execution>
<phase>install</phase>
<goals>
<goal>build-image</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,13 +0,0 @@
package com.food.order.system.customer.service;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = "com.food.order")
public class CustomerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CustomerServiceApplication.class, args);
}
}

View File

@@ -1,22 +0,0 @@
server:
port: 8184
spring:
jpa:
open-in-view: false
show-sql: false
database-platform: org.hibernate.dialect.PostgreSQL9Dialect
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQL9Dialect
datasource:
url: jdbc:postgresql://localhost:5432/postgres?currentSchema=customer&binaryTransfer=true&reWriteBatchedInserts=true
username: postgres
password: postgres
driver-class-name: org.postgresql.Driver
sql:
init:
mode: always
schema-locations: classpath:init-schema.sql
data-locations: classpath:init-data.sql
platform: postgres

View File

@@ -1,5 +0,0 @@
INSERT INTO customer.customers(id, username, first_name, last_name)
VALUES ('d215b5f8-0249-4dc5-89a3-51fd148cfb41', 'user_1', 'First', 'User');
INSERT INTO customer.customers(id, username, first_name, last_name)
VALUES ('d215b5f8-0249-4dc5-89a3-51fd148cfb42', 'user_2', 'Second', 'User');

View File

@@ -13,12 +13,14 @@ services:
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
kafka-topics --bootstrap-server kafka-broker-1:9092 --topic customer-value --delete --if-exists
echo -e 'Creating kafka topics'
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
kafka-topics --bootstrap-server kafka-broker-1:9092 --create --if-not-exists --topic customer-value --replication-factor 3 --partitions 3
echo -e 'Successfully created the following topics:'

View File

@@ -0,0 +1,512 @@
/**
* Autogenerated by Avro
*
* DO NOT EDIT DIRECTLY
*/
package com.food.order.system.kafka.order.avro.model;
import org.apache.avro.message.BinaryMessageDecoder;
import org.apache.avro.message.BinaryMessageEncoder;
import org.apache.avro.message.SchemaStore;
import org.apache.avro.specific.SpecificData;
@org.apache.avro.specific.AvroGenerated
public class CustomerAvroModel extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord {
private static final long serialVersionUID = 7953499667909084838L;
public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"CustomerAvroModel\",\"namespace\":\"com.food.order.system.kafka.order.avro.model\",\"fields\":[{\"name\":\"id\",\"type\":{\"type\":\"string\",\"logicalType\":\"uuid\"}},{\"name\":\"username\",\"type\":{\"type\":\"string\",\"avro.java.string\":\"String\"}},{\"name\":\"firstName\",\"type\":{\"type\":\"string\",\"avro.java.string\":\"String\"}},{\"name\":\"lastName\",\"type\":{\"type\":\"string\",\"avro.java.string\":\"String\"}}]}");
public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; }
private static final SpecificData MODEL$ = new SpecificData();
private static final BinaryMessageEncoder<CustomerAvroModel> ENCODER =
new BinaryMessageEncoder<CustomerAvroModel>(MODEL$, SCHEMA$);
private static final BinaryMessageDecoder<CustomerAvroModel> DECODER =
new BinaryMessageDecoder<CustomerAvroModel>(MODEL$, SCHEMA$);
/**
* Return the BinaryMessageEncoder instance used by this class.
* @return the message encoder used by this class
*/
public static BinaryMessageEncoder<CustomerAvroModel> getEncoder() {
return ENCODER;
}
/**
* Return the BinaryMessageDecoder instance used by this class.
* @return the message decoder used by this class
*/
public static BinaryMessageDecoder<CustomerAvroModel> getDecoder() {
return DECODER;
}
/**
* Create a new BinaryMessageDecoder instance for this class that uses the specified {@link SchemaStore}.
* @param resolver a {@link SchemaStore} used to find schemas by fingerprint
* @return a BinaryMessageDecoder instance for this class backed by the given SchemaStore
*/
public static BinaryMessageDecoder<CustomerAvroModel> createDecoder(SchemaStore resolver) {
return new BinaryMessageDecoder<CustomerAvroModel>(MODEL$, SCHEMA$, resolver);
}
/**
* Serializes this CustomerAvroModel to a ByteBuffer.
* @return a buffer holding the serialized data for this instance
* @throws java.io.IOException if this instance could not be serialized
*/
public java.nio.ByteBuffer toByteBuffer() throws java.io.IOException {
return ENCODER.encode(this);
}
/**
* Deserializes a CustomerAvroModel from a ByteBuffer.
* @param b a byte buffer holding serialized data for an instance of this class
* @return a CustomerAvroModel instance decoded from the given buffer
* @throws java.io.IOException if the given bytes could not be deserialized into an instance of this class
*/
public static CustomerAvroModel fromByteBuffer(
java.nio.ByteBuffer b) throws java.io.IOException {
return DECODER.decode(b);
}
private java.lang.String id;
private java.lang.String username;
private java.lang.String firstName;
private java.lang.String lastName;
/**
* Default constructor. Note that this does not initialize fields
* to their default values from the schema. If that is desired then
* one should use <code>newBuilder()</code>.
*/
public CustomerAvroModel() {}
/**
* All-args constructor.
* @param id The new value for id
* @param username The new value for username
* @param firstName The new value for firstName
* @param lastName The new value for lastName
*/
public CustomerAvroModel(java.lang.String id, java.lang.String username, java.lang.String firstName, java.lang.String lastName) {
this.id = id;
this.username = username;
this.firstName = firstName;
this.lastName = lastName;
}
public org.apache.avro.specific.SpecificData getSpecificData() { return MODEL$; }
public org.apache.avro.Schema getSchema() { return SCHEMA$; }
// Used by DatumWriter. Applications should not call.
public java.lang.Object get(int field$) {
switch (field$) {
case 0: return id;
case 1: return username;
case 2: return firstName;
case 3: return lastName;
default: throw new IndexOutOfBoundsException("Invalid index: " + field$);
}
}
private static final org.apache.avro.Conversion<?>[] conversions =
new org.apache.avro.Conversion<?>[] {
null,
null,
null,
null,
null
};
@Override
public org.apache.avro.Conversion<?> getConversion(int field) {
return conversions[field];
}
// Used by DatumReader. Applications should not call.
@SuppressWarnings(value="unchecked")
public void put(int field$, java.lang.Object value$) {
switch (field$) {
case 0: id = value$ != null ? value$.toString() : null; break;
case 1: username = value$ != null ? value$.toString() : null; break;
case 2: firstName = value$ != null ? value$.toString() : null; break;
case 3: lastName = value$ != null ? value$.toString() : null; break;
default: throw new IndexOutOfBoundsException("Invalid index: " + field$);
}
}
/**
* Gets the value of the 'id' field.
* @return The value of the 'id' field.
*/
public java.lang.String getId() {
return id;
}
/**
* Sets the value of the 'id' field.
* @param value the value to set.
*/
public void setId(java.lang.String value) {
this.id = value;
}
/**
* Gets the value of the 'username' field.
* @return The value of the 'username' field.
*/
public java.lang.String getUsername() {
return username;
}
/**
* Sets the value of the 'username' field.
* @param value the value to set.
*/
public void setUsername(java.lang.String value) {
this.username = value;
}
/**
* Gets the value of the 'firstName' field.
* @return The value of the 'firstName' field.
*/
public java.lang.String getFirstName() {
return firstName;
}
/**
* Sets the value of the 'firstName' field.
* @param value the value to set.
*/
public void setFirstName(java.lang.String value) {
this.firstName = value;
}
/**
* Gets the value of the 'lastName' field.
* @return The value of the 'lastName' field.
*/
public java.lang.String getLastName() {
return lastName;
}
/**
* Sets the value of the 'lastName' field.
* @param value the value to set.
*/
public void setLastName(java.lang.String value) {
this.lastName = value;
}
/**
* Creates a new CustomerAvroModel RecordBuilder.
* @return A new CustomerAvroModel RecordBuilder
*/
public static com.food.order.system.kafka.order.avro.model.CustomerAvroModel.Builder newBuilder() {
return new com.food.order.system.kafka.order.avro.model.CustomerAvroModel.Builder();
}
/**
* Creates a new CustomerAvroModel RecordBuilder by copying an existing Builder.
* @param other The existing builder to copy.
* @return A new CustomerAvroModel RecordBuilder
*/
public static com.food.order.system.kafka.order.avro.model.CustomerAvroModel.Builder newBuilder(com.food.order.system.kafka.order.avro.model.CustomerAvroModel.Builder other) {
if (other == null) {
return new com.food.order.system.kafka.order.avro.model.CustomerAvroModel.Builder();
} else {
return new com.food.order.system.kafka.order.avro.model.CustomerAvroModel.Builder(other);
}
}
/**
* Creates a new CustomerAvroModel RecordBuilder by copying an existing CustomerAvroModel instance.
* @param other The existing instance to copy.
* @return A new CustomerAvroModel RecordBuilder
*/
public static com.food.order.system.kafka.order.avro.model.CustomerAvroModel.Builder newBuilder(com.food.order.system.kafka.order.avro.model.CustomerAvroModel other) {
if (other == null) {
return new com.food.order.system.kafka.order.avro.model.CustomerAvroModel.Builder();
} else {
return new com.food.order.system.kafka.order.avro.model.CustomerAvroModel.Builder(other);
}
}
/**
* RecordBuilder for CustomerAvroModel instances.
*/
@org.apache.avro.specific.AvroGenerated
public static class Builder extends org.apache.avro.specific.SpecificRecordBuilderBase<CustomerAvroModel>
implements org.apache.avro.data.RecordBuilder<CustomerAvroModel> {
private java.lang.String id;
private java.lang.String username;
private java.lang.String firstName;
private java.lang.String lastName;
/** Creates a new Builder */
private Builder() {
super(SCHEMA$, MODEL$);
}
/**
* Creates a Builder by copying an existing Builder.
* @param other The existing Builder to copy.
*/
private Builder(com.food.order.system.kafka.order.avro.model.CustomerAvroModel.Builder other) {
super(other);
if (isValidValue(fields()[0], other.id)) {
this.id = data().deepCopy(fields()[0].schema(), other.id);
fieldSetFlags()[0] = other.fieldSetFlags()[0];
}
if (isValidValue(fields()[1], other.username)) {
this.username = data().deepCopy(fields()[1].schema(), other.username);
fieldSetFlags()[1] = other.fieldSetFlags()[1];
}
if (isValidValue(fields()[2], other.firstName)) {
this.firstName = data().deepCopy(fields()[2].schema(), other.firstName);
fieldSetFlags()[2] = other.fieldSetFlags()[2];
}
if (isValidValue(fields()[3], other.lastName)) {
this.lastName = data().deepCopy(fields()[3].schema(), other.lastName);
fieldSetFlags()[3] = other.fieldSetFlags()[3];
}
}
/**
* Creates a Builder by copying an existing CustomerAvroModel instance
* @param other The existing instance to copy.
*/
private Builder(com.food.order.system.kafka.order.avro.model.CustomerAvroModel other) {
super(SCHEMA$, MODEL$);
if (isValidValue(fields()[0], other.id)) {
this.id = data().deepCopy(fields()[0].schema(), other.id);
fieldSetFlags()[0] = true;
}
if (isValidValue(fields()[1], other.username)) {
this.username = data().deepCopy(fields()[1].schema(), other.username);
fieldSetFlags()[1] = true;
}
if (isValidValue(fields()[2], other.firstName)) {
this.firstName = data().deepCopy(fields()[2].schema(), other.firstName);
fieldSetFlags()[2] = true;
}
if (isValidValue(fields()[3], other.lastName)) {
this.lastName = data().deepCopy(fields()[3].schema(), other.lastName);
fieldSetFlags()[3] = true;
}
}
/**
* Gets the value of the 'id' field.
* @return The value.
*/
public java.lang.String getId() {
return id;
}
/**
* Sets the value of the 'id' field.
* @param value The value of 'id'.
* @return This builder.
*/
public com.food.order.system.kafka.order.avro.model.CustomerAvroModel.Builder setId(java.lang.String value) {
validate(fields()[0], value);
this.id = value;
fieldSetFlags()[0] = true;
return this;
}
/**
* Checks whether the 'id' field has been set.
* @return True if the 'id' field has been set, false otherwise.
*/
public boolean hasId() {
return fieldSetFlags()[0];
}
/**
* Clears the value of the 'id' field.
* @return This builder.
*/
public com.food.order.system.kafka.order.avro.model.CustomerAvroModel.Builder clearId() {
id = null;
fieldSetFlags()[0] = false;
return this;
}
/**
* Gets the value of the 'username' field.
* @return The value.
*/
public java.lang.String getUsername() {
return username;
}
/**
* Sets the value of the 'username' field.
* @param value The value of 'username'.
* @return This builder.
*/
public com.food.order.system.kafka.order.avro.model.CustomerAvroModel.Builder setUsername(java.lang.String value) {
validate(fields()[1], value);
this.username = value;
fieldSetFlags()[1] = true;
return this;
}
/**
* Checks whether the 'username' field has been set.
* @return True if the 'username' field has been set, false otherwise.
*/
public boolean hasUsername() {
return fieldSetFlags()[1];
}
/**
* Clears the value of the 'username' field.
* @return This builder.
*/
public com.food.order.system.kafka.order.avro.model.CustomerAvroModel.Builder clearUsername() {
username = null;
fieldSetFlags()[1] = false;
return this;
}
/**
* Gets the value of the 'firstName' field.
* @return The value.
*/
public java.lang.String getFirstName() {
return firstName;
}
/**
* Sets the value of the 'firstName' field.
* @param value The value of 'firstName'.
* @return This builder.
*/
public com.food.order.system.kafka.order.avro.model.CustomerAvroModel.Builder setFirstName(java.lang.String value) {
validate(fields()[2], value);
this.firstName = value;
fieldSetFlags()[2] = true;
return this;
}
/**
* Checks whether the 'firstName' field has been set.
* @return True if the 'firstName' field has been set, false otherwise.
*/
public boolean hasFirstName() {
return fieldSetFlags()[2];
}
/**
* Clears the value of the 'firstName' field.
* @return This builder.
*/
public com.food.order.system.kafka.order.avro.model.CustomerAvroModel.Builder clearFirstName() {
firstName = null;
fieldSetFlags()[2] = false;
return this;
}
/**
* Gets the value of the 'lastName' field.
* @return The value.
*/
public java.lang.String getLastName() {
return lastName;
}
/**
* Sets the value of the 'lastName' field.
* @param value The value of 'lastName'.
* @return This builder.
*/
public com.food.order.system.kafka.order.avro.model.CustomerAvroModel.Builder setLastName(java.lang.String value) {
validate(fields()[3], value);
this.lastName = value;
fieldSetFlags()[3] = true;
return this;
}
/**
* Checks whether the 'lastName' field has been set.
* @return True if the 'lastName' field has been set, false otherwise.
*/
public boolean hasLastName() {
return fieldSetFlags()[3];
}
/**
* Clears the value of the 'lastName' field.
* @return This builder.
*/
public com.food.order.system.kafka.order.avro.model.CustomerAvroModel.Builder clearLastName() {
lastName = null;
fieldSetFlags()[3] = false;
return this;
}
@Override
@SuppressWarnings("unchecked")
public CustomerAvroModel build() {
try {
CustomerAvroModel record = new CustomerAvroModel();
record.id = fieldSetFlags()[0] ? this.id : (java.lang.String) defaultValue(fields()[0]);
record.username = fieldSetFlags()[1] ? this.username : (java.lang.String) defaultValue(fields()[1]);
record.firstName = fieldSetFlags()[2] ? this.firstName : (java.lang.String) defaultValue(fields()[2]);
record.lastName = fieldSetFlags()[3] ? this.lastName : (java.lang.String) defaultValue(fields()[3]);
return record;
} catch (org.apache.avro.AvroMissingFieldException e) {
throw e;
} catch (java.lang.Exception e) {
throw new org.apache.avro.AvroRuntimeException(e);
}
}
}
@SuppressWarnings("unchecked")
private static final org.apache.avro.io.DatumWriter<CustomerAvroModel>
WRITER$ = (org.apache.avro.io.DatumWriter<CustomerAvroModel>)MODEL$.createDatumWriter(SCHEMA$);
@Override public void writeExternal(java.io.ObjectOutput out)
throws java.io.IOException {
WRITER$.write(this, SpecificData.getEncoder(out));
}
@SuppressWarnings("unchecked")
private static final org.apache.avro.io.DatumReader<CustomerAvroModel>
READER$ = (org.apache.avro.io.DatumReader<CustomerAvroModel>)MODEL$.createDatumReader(SCHEMA$);
@Override public void readExternal(java.io.ObjectInput in)
throws java.io.IOException {
READER$.read(this, SpecificData.getDecoder(in));
}
}

View File

@@ -0,0 +1,32 @@
{
"namespace": "com.food.order.system.kafka.order.avro.model",
"type": "record",
"name": "CustomerAvroModel",
"fields": [
{
"name": "id",
"type": {
"type": "string",
"logicalType": "uuid"
}
},
{
"name": "username",
"type": {
"type": "string"
}
},
{
"name": "firstName",
"type": {
"type": "string"
}
},
{
"name": "lastName",
"type": {
"type": "string"
}
}
]
}

View File

@@ -9,6 +9,7 @@ order-service:
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
customer-topic-name: customer
outbox-scheduler-fixed-rate: 10000
outbox-scheduler-initial-delay: 10000
@@ -54,6 +55,7 @@ kafka-consumer-config:
value-deserializer: io.confluent.kafka.serializers.KafkaAvroDeserializer
payment-consumer-group-id: payment-topic-consumer
restaurant-approval-consumer-group-id: restaurant-approval-topic-consumer
customer-group-id: customer-topic-consumer
auto-offset-reset: earliest
specific-avro-reader-key: specific.avro.reader
specific-avro-reader: true

View File

@@ -36,9 +36,9 @@ CREATE TABLE "order".order_items
ALTER TABLE "order".order_items
ADD CONSTRAINT "FK_ORDER_ID" FOREIGN KEY (order_id)
REFERENCES "order".orders (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE CASCADE
REFERENCES "order".orders (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE CASCADE
NOT VALID;
DROP TABLE IF EXISTS "order".order_address CASCADE;
@@ -55,17 +55,16 @@ CREATE TABLE "order".order_address
ALTER TABLE "order".order_address
ADD CONSTRAINT "FK_ORDER_ID" FOREIGN KEY (order_id)
REFERENCES "order".orders (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE CASCADE
REFERENCES "order".orders (id) MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE CASCADE
NOT VALID;
DROP TYPE IF EXISTS saga_status;
CREATE TYPE saga_status AS ENUM ('STARTED','FAILED','SUCCEEDED','PROCESSING', 'COMPENSATING','COMPENSATED');
CREATE TYPE saga_status AS ENUM ('STARTED', 'FAILED', 'SUCCEEDED', 'PROCESSING', 'COMPENSATING', 'COMPENSATED');
DROP TYPE IF EXISTS outbox_status;
CREATE TYPE outbox_status AS ENUM ('STARTED','COMPLETED','FAILED');
CREATE TYPE outbox_status AS ENUM ('STARTED', 'COMPLETED', 'FAILED');
DROP TABLE IF EXISTS "order".payment_outbox CASCADE;
@@ -88,10 +87,9 @@ CREATE INDEX "payment_outbox_saga_status"
ON "order".payment_outbox
(type, outbox_status, saga_status);
CREATE UNIQUE INDEX "payment_outbox_saga_id"
ON "order".payment_outbox
(type, saga_id, saga_status);
--CREATE UNIQUE INDEX "payment_outbox_saga_id"
-- ON "order".payment_outbox
-- (type, saga_id, saga_status);
DROP TABLE IF EXISTS "order".restaurant_approval_outbox CASCADE;
@@ -114,6 +112,17 @@ CREATE INDEX "restaurant_approval_outbox_saga_status"
ON "order".restaurant_approval_outbox
(type, outbox_status, saga_status);
CREATE UNIQUE INDEX "restaurant_approval_outbox_saga_id"
ON "order".restaurant_approval_outbox
(type, saga_id, saga_status);
--CREATE UNIQUE INDEX "restaurant_approval_outbox_saga_id"
-- ON "order".restaurant_approval_outbox
-- (type, saga_id, saga_status);
DROP TABLE IF EXISTS "order".customers CASCADE;
CREATE TABLE "order".customers
(
id uuid NOT NULL,
username character varying COLLATE pg_catalog."default" NOT NULL,
first_name character varying COLLATE pg_catalog."default" NOT NULL,
last_name character varying COLLATE pg_catalog."default" NOT NULL,
CONSTRAINT customers_pkey PRIMARY KEY (id)
);

View File

@@ -19,6 +19,12 @@ public class CustomerRepositoryImpl implements CustomerRepository {
private final CustomerDataAccessMapper customerDataAccessMapper;
@Override
public Customer save(Customer customer) {
return customerDataAccessMapper.customerEntityToCustomer(
customerJpaRepository.save(customerDataAccessMapper.customerToCustomerEntity(customer)));
}
@Override
public Optional<Customer> findCustomer(UUID customerId) {
return customerJpaRepository.findById(customerId)

View File

@@ -10,7 +10,7 @@ import java.util.UUID;
@Entity
@DynamicUpdate
@Table(name = "order_customer_m_view",schema = "customer")
@Table(name = "customers")
@Getter
@Setter
@Builder
@@ -20,4 +20,8 @@ public class CustomerEntity {
@Id
private UUID id;
private String username;
private String firstName;
private String lastName;
}

View File

@@ -11,4 +11,13 @@ public class CustomerDataAccessMapper {
public Customer customerEntityToCustomer(CustomerEntity customerEntity) {
return new Customer(new CustomerId(customerEntity.getId()));
}
public CustomerEntity customerToCustomerEntity(Customer customer) {
return CustomerEntity.builder()
.id(customer.getId().getValue())
.firstName(customer.getFirstName())
.lastName(customer.getLastName())
.username(customer.getUsername())
.build();
}
}

View File

@@ -0,0 +1,37 @@
package com.food.order.system;
import com.food.order.system.dto.message.CustomerModel;
import com.food.order.system.mapper.OrderDataMapper;
import com.food.order.system.ports.input.message.listener.customer.CustomerMessageListener;
import com.food.order.system.ports.output.repository.CustomerRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Objects;
@Service
@Slf4j
@RequiredArgsConstructor
public class CustomerMessageListenerImpl implements CustomerMessageListener {
private final CustomerRepository customerRepository;
private final OrderDataMapper orderDataMapper;
@Override
public void customerCreated(CustomerModel customerModel) {
var customer = customerRepository.save
(orderDataMapper.customerModelToCustomer(customerModel));
if (Objects.isNull(customer)) {
log.error("Customer not created");
} else {
log.info("Customer created");
}
}
}

View File

@@ -0,0 +1,15 @@
package com.food.order.system.dto.message;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
@AllArgsConstructor
public class CustomerModel {
private String id;
private String username;
private String firstName;
private String lastName;
}

View File

@@ -1,9 +1,6 @@
package com.food.order.system.mapper;
import com.food.order.system.domain.entity.Order;
import com.food.order.system.domain.entity.OrderItem;
import com.food.order.system.domain.entity.Product;
import com.food.order.system.domain.entity.Restaurant;
import com.food.order.system.domain.entity.*;
import com.food.order.system.domain.event.OrderCancelledEvent;
import com.food.order.system.domain.event.OrderCreatedEvent;
import com.food.order.system.domain.event.OrderPaidEvent;
@@ -11,6 +8,7 @@ import com.food.order.system.domain.valueobject.StreetAddress;
import com.food.order.system.dto.create.CreateOrderCommand;
import com.food.order.system.dto.create.CreateOrderResponse;
import com.food.order.system.dto.create.OrderAddress;
import com.food.order.system.dto.message.CustomerModel;
import com.food.order.system.dto.track.TrackOrderResponse;
import com.food.order.system.outbox.model.approval.OrderApprovalEventPayload;
import com.food.order.system.outbox.model.approval.OrderApprovalProduct;
@@ -24,6 +22,14 @@ import java.util.UUID;
@Component
public class OrderDataMapper {
public Customer customerModelToCustomer(CustomerModel customerModel) {
return new Customer(new CustomerId(UUID.fromString(customerModel.getId())),
customerModel.getUsername(),
customerModel.getFirstName(),
customerModel.getLastName());
}
public OrderPaymentEventPayload orderCancelledEventToOrderPaymentEventPayload(
OrderCancelledEvent orderCancelledEvent) {
return OrderPaymentEventPayload.builder()

View File

@@ -0,0 +1,7 @@
package com.food.order.system.ports.input.message.listener.customer;
import com.food.order.system.dto.message.CustomerModel;
public interface CustomerMessageListener {
void customerCreated(CustomerModel customerModel);
}

View File

@@ -6,5 +6,7 @@ import java.util.Optional;
import java.util.UUID;
public interface CustomerRepository {
Customer save(Customer customer);
Optional<Customer> findCustomer(UUID customerId);
}

View File

@@ -5,10 +5,33 @@ import com.food.order.system.valueobject.CustomerId;
public class Customer extends AggregateRoot<CustomerId> {
public Customer(){
private String username;
private String firstName;
private String lastName;
public Customer(CustomerId id, String username, String firstName, String lastName) {
super.setId(id);
this.username = username;
this.firstName = firstName;
this.lastName = lastName;
}
public Customer (CustomerId id) {
super.setId(id);
}
public String getUsername() {
return username;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}

View File

@@ -0,0 +1,42 @@
package com.food.order.system.messaging.listener.kafka;
import com.food.order.system.kafka.consumer.KafkaConsumer;
import com.food.order.system.kafka.order.avro.model.CustomerAvroModel;
import com.food.order.system.messaging.mapper.OrderMessagingDataMapper;
import com.food.order.system.ports.input.message.listener.customer.CustomerMessageListener;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
public class CustomerKafkaListener implements KafkaConsumer<CustomerAvroModel> {
private final CustomerMessageListener customerMessageListener;
private final OrderMessagingDataMapper orderMessagingDataMapper;
@Override
@KafkaListener(id = "${kafka-consumer-config.customer-group-id}",
topics = "${order-service.customer-topic-name}")
public void receive(List<CustomerAvroModel> messages,
List<String> keys,
List<Integer> partitions,
List<Long> offSets) {
log.info("{} number of customer create messages received with keys {}, partitions {} and offsets {}",
messages.size(),
keys.toString(),
partitions.toString(),
offSets.toString());
messages.forEach(customerAvroModel ->
customerMessageListener.customerCreated(orderMessagingDataMapper
.customerAvroModelToCustomerModel(customerAvroModel)));
}
}

View File

@@ -1,5 +1,6 @@
package com.food.order.system.messaging.mapper;
import com.food.order.system.dto.message.CustomerModel;
import com.food.order.system.dto.message.PaymentResponse;
import com.food.order.system.dto.message.RestaurantApprovalResponse;
import com.food.order.system.kafka.order.avro.model.*;
@@ -75,4 +76,13 @@ public class OrderMessagingDataMapper {
}
public CustomerModel customerAvroModelToCustomerModel(CustomerAvroModel customerAvroModel) {
return CustomerModel.builder()
.id(customerAvroModel.getId())
.username(customerAvroModel.getUsername())
.firstName(customerAvroModel.getFirstName())
.lastName(customerAvroModel.getLastName())
.build();
}
}

36
pom.xml
View File

@@ -28,7 +28,7 @@
<properties>
<maven-compiler-plugin.version>3.9.0</maven-compiler-plugin.version>
<mockito.version>4.5.1</mockito.version>
<mockito.version>4.3.1</mockito.version>
<spring-kafka.version>2.8.2</spring-kafka.version>
<kavka-avro.serializer.version>7.0.1</kavka-avro.serializer.version>
<avro.version>1.11.0</avro.version>
@@ -49,6 +49,40 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>customer-domain-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>customer-service-application</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>customer-application</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>customer-data-access</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>customer-messaging</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.food.order</groupId>
<artifactId>payment-domain</artifactId>