enhancements + removal of unnecessary code

This commit is contained in:
Muhammad Ali
2023-03-18 00:02:14 +04:00
parent 3d2cb078de
commit a319e51126
79 changed files with 902 additions and 3579 deletions

View File

@@ -42,15 +42,15 @@ Your domain on the other hand will always be there.
should be modified following strong consistency principle (oppose to domain events
and eventual consistency for cross aggregation root changes).
## Use cases
### 1. Communication between layers
On this demo we'll try to execute a simple business case of submitting an order
which will be initially handled by the infrastructure layer and then passed
to domain layer to be processed (Where business happens), then return the result
back to infrastructure layer.
## Deployment notes
Environment Variables:
* host
* keycloak.credentials.secret
This project uses embedded DB: [h2-console](http://localhost:8081/h2-console).
You can test via requests found on this [postman collections file](ddd.postman_collection.json).
## Authors
[![Linkedin](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white&label=Muhammad%20Ali)](https://linkedin.com/in/zatribune)

View File

@@ -1,10 +0,0 @@
# Getting Started
### Reference Documentation
For further reference, please consider the following sections:
* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)
* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.6.7/maven-plugin/reference/html/)
* [Create an OCI image](https://docs.spring.io/spring-boot/docs/2.6.7/maven-plugin/reference/html/#build-image)

View File

@@ -1,101 +0,0 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.tribune</groupId>
<artifactId>backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>backend-api-definition</artifactId>
<properties>
<swagger-annotations-version>1.6.6</swagger-annotations-version>
<maven-plugin-version>1.0.0</maven-plugin-version>
<springfox-version>2.9.2</springfox-version>
<spring-boot-starter-test-version>2.6.7</spring-boot-starter-test-version>
<spring-boot-starter-web-version>2.6.7</spring-boot-starter-web-version>
<jackson-databind-nullable>0.2.2</jackson-databind-nullable>
</properties>
<dependencies>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${swagger-annotations-version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot-starter-web-version}</version>
</dependency>
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>${jackson-databind-nullable}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>4.3.1</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>
${project.basedir}/src/main/resources/tribune/reporting-service.yml
</inputSpec>
<generatorName>spring</generatorName>
<apiPackage>com.tribune.backend.infrastructure.api</apiPackage>
<!-- We should output to the general one -->
<!-- <modelPackage>com.example.cas.restapi.model</modelPackage>-->
<modelPackage>com.tribune.backend.infrastructure.model</modelPackage>
<supportingFilesToGenerate>
ApiUtil.java
</supportingFilesToGenerate>
<configOptions>
<delegatePattern>true</delegatePattern>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<proc>none</proc>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,22 +0,0 @@
openapi: "3.0.3"
info:
title: "reporting API"
description: "reporting API"
version: "1.0.0"
servers:
- url: "https://reporting"
paths:
/reporting/generate/{type}:
post:
summary: "POST reporting/generate/{type}"
operationId: "generate"
parameters:
- name: "type"
in: "path"
required: true
schema:
type: string
example: "PDF"
responses:
"200":
description: "OK"

View File

@@ -1,19 +0,0 @@
## Notes regarding keycloak deployment
### Database
* Keycloak is integrated with the main postgres instance of our backend, but connected to a different database.
* Keycloak can access this database via a dedicated user.
* Both Keycloak's user and database are initialized within this script file [create_keycloak_db](create_keycloak_db.sh) and mounted on start.
* Scripts in `/docker-entrypoint-initdb.d` are only run if you start
the container with a data directory that is empty; any pre-existing database
will be left untouched on container startup.
* One common problem is that if one of your `/docker-entrypoint-initdb.d`
scripts fails (which will cause the entrypoint script to exit) and your orchestrator restarts the container with the already initialized data directory, it will not continue on with your scripts.
* Any `*.sql` files will be executed by `POSTGRES_USER`, which defaults to the postgres **superuser**. So it is recommended that any `psql` commands that are run inside a `*.sh` script be executed
as `POSTGRES_USER` by using the `--username "$POSTGRES_USER"` flag.
* This newly created user is supposed to be able to connect without a password due to the presence of
trust authentication for Unix socket connections made inside the container (**tested and failed -> needs a password on user creation**).
### Initial Configuration
* This file [realm-export.json](keycloak-init/realm-export.json) is used as a backup for realm configuration. It can be loaded from the admin console.

View File

@@ -1,9 +0,0 @@
#!/bin/bash
set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER keycloak WITH ENCRYPTED PASSWORD 'keycloak';
CREATE DATABASE keycloak_db;
GRANT ALL PRIVILEGES ON DATABASE keycloak_db TO keycloak;
EOSQL

View File

@@ -1,72 +0,0 @@
version: '3.8'
networks:
tribune-network:
name: tribune-network
services:
app:
image: 'tribune-backend:latest'
build:
context: ../../../../..
dockerfile: ./infrastructure/docker/dev/Dockerfile
container_name: app
depends_on:
- db
ports:
- '8880:8880'
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/tribune_db_docker
- SPRING_DATASOURCE_USERNAME=tribune_db_user
- SPRING_DATASOURCE_PASSWORD=password
labels:
collect_logs_with_filebeat: 'true'
decode_log_event_to_json_object: 'true'
networks:
- tribune-network
db:
image: 'postgres:13.1-alpine'
restart: unless-stopped
container_name: db
environment:
#default superuser
POSTGRES_USER: tribune_db_user
##default superuser password
POSTGRES_PASSWORD: password
##default database
POSTGRES_DB: tribune_db_docker
ports:
- '5432:5432'
volumes:
- ./db:/var/lib/postgresql/data
- ./create_keycloak_db.sh:/docker-entrypoint-initdb.d/create_keycloak_db.sh
labels:
collect_logs_with_filebeat: 'true'
decode_log_event_to_json_object: 'true'
networks:
- tribune-network
keycloak:
image: bitnami/keycloak:18
container_name: keycloak
environment:
DB_VENDOR: POSTGRES
#by service name
DB_ADDR: db
#database name
DB_DATABASE: keycloak_db
DB_USER: keycloak
DB_PASSWORD: keycloak
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: Pass@1234
# Uncomment the line below if you want to specify JDBC parameters. The parameter below is just an example, and it shouldn't be used in production without knowledge. It is highly recommended that you read the PostgreSQL JDBC driver documentation in order to use it.
#JDBC_PARAMS: "ssl=true"
ports:
- "8080:8080"
depends_on:
- db
networks:
- tribune-network

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +0,0 @@
## Spring embedded webserver
server:
port: 9090
error:
whitelabel:
enabled: false
spring:
## Spring datasource config
datasource:
url: jdbc:h2:mem:testdb
username: admin
password:
# H2 specific
h2:
console:
enabled: true
jpa:
hibernate:
ddl-auto: create-drop
open-in-view: false

View File

@@ -1,46 +1,23 @@
server:
port: 8081
spring:
application:
name: tribune
mvc:
throw-exception-if-no-handler-found: true
management:
endpoints:
web:
exposure:
include: *
#keycloak user client properties
keycloak:
auth-server-url: http://${host}:8080/auth/
realm: tribune
resource: tribune-user
credentials:
secret: ${keycloak.credentials.secret}
ssl-required: external
principal-attribute: preferred_username
use-resource-role-mappings: true
bearer-only: true
#keycloak.policy-enforcer-config.enforcement-mode=ENFORCING
#keycloak admin client properties
tribune:
keycloak:
client:
user-management:
resource: tribune-user-management
client-secret: oT9cm9JhbJH5owI444FxS4DPKLdIpKel
username: users.admin@paywithtribune.com
password: 1234
#app.cors.allowed-origins=https://localhost:8880
#if needed for any reason by frontend
#app.cors.allowed-origins: http://localhost:port
logging:
level:
org:
keycloak: DEBUG
springframework:
security: DEBUG
#spring.jackson.serialization.fail-on-empty-beans=false
name: ddd-application
## Spring datasource config
datasource:
url: jdbc:h2:mem:testdb
username: admin
password:
driver-class-name: org.h2.Driver
h2:
console:
enabled: true
jpa:
hibernate:
ddl-auto: create-drop
properties:
hibernate:
dialect: org.hibernate.dialect.H2Dialect
format_sql: true
show-sql: true
open-in-view: false

View File

@@ -2,23 +2,24 @@
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>backend-domain</artifactId>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<parent>
<groupId>com.tribune</groupId>
<artifactId>backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>backend-domain</artifactId>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
@@ -40,6 +41,16 @@
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
@@ -52,6 +63,26 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>

View File

@@ -1,8 +0,0 @@
package com.tribune.backend.domain;
/**
* Indicates any movable object that somehow relates to the domain model.
* Top-level marker interface for all domain objects.
*/
public interface DomainObject {
}

View File

@@ -1,12 +1,14 @@
package com.tribune.backend.domain.context;
import com.tribune.backend.domain.AggregateRoot;
import com.tribune.backend.domain.dto.customer.Customer;
import com.tribune.backend.domain.dto.order.Order;
import com.tribune.backend.domain.element.AggregateRoot;
import com.tribune.backend.domain.element.customer.Customer;
import com.tribune.backend.domain.element.order.Order;
import com.tribune.backend.domain.element.order.lineitem.product.Product;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.SuperBuilder;
import java.util.List;
import java.util.UUID;
@Data
@@ -17,4 +19,6 @@ public class CustomerOrder extends AggregateRoot<UUID> {
private Customer customer;
private Order order;
private List<Product>products;
}

View File

@@ -1,27 +1,22 @@
package com.tribune.backend.domain.dto;
import com.tribune.backend.domain.enums.BlogStatus;
import com.tribune.backend.domain.element.order.Order;
import com.tribune.backend.domain.element.order.lineitem.product.Product;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
import java.math.BigDecimal;
import java.util.List;
@Data
@Builder
public class SingleOrderResponse {
private String id;
private Order order;
private String title;
private BigDecimal totalCost;
private String content;
private List<Product> products;
private BlogStatus status;
private String appUser;
private LocalDateTime creationTimestamp;
private LocalDateTime updateTimestamp;
}

View File

@@ -1,10 +1,9 @@
package com.tribune.backend.domain.dto;
import com.tribune.backend.domain.element.order.Order;
import lombok.*;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@Data
@@ -12,15 +11,15 @@ import javax.validation.constraints.NotNull;
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class PlaceOrderRequest {
public class SubmitOrderRequest {
@NotNull(message = "Content is missing.")
@Min(value = 20)
@NotBlank(message = "Content is missing.")
private String content;
@NotNull
@NotEmpty
@NotBlank
private String user;
private Long user;
@NotNull
private Order order;
}

View File

@@ -1,38 +0,0 @@
package com.tribune.backend.domain.dto.customer;
import com.tribune.backend.domain.Entity;
import com.tribune.backend.domain.dto.customer.address.Address;
import com.tribune.backend.domain.dto.customer.payment.Payment;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import java.util.List;
import java.util.UUID;
@Getter
@Setter
@SuperBuilder
@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
public class Customer extends Entity<UUID> {
private UUID id;
private String displayName;
private String firstName;
private String lastName;
private CustomerState state;
private CustomerType type;
//todo: like Address
private List<Payment> paymentList;
private List<Address> addressList;
}

View File

@@ -1,19 +0,0 @@
package com.tribune.backend.domain.dto.customer.invoice;
import com.tribune.backend.domain.dto.customer.Customer;
import lombok.*;
import java.util.UUID;
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Invoice {
private UUID id;
private Customer customer;
}

View File

@@ -1,26 +0,0 @@
package com.tribune.backend.domain.dto.order;
import com.tribune.backend.domain.Entity;
import com.tribune.backend.domain.dto.order.lineitem.LineItem;
import com.tribune.backend.domain.enums.OrderStatus;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import java.util.List;
import java.util.UUID;
@Setter
@Getter
@SuperBuilder
@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
public class Order extends Entity<UUID> {
private UUID id;
private OrderStatus status;
private List<LineItem> lineItemEntities;
}

View File

@@ -1,22 +0,0 @@
package com.tribune.backend.domain.dto.order.lineitem;
import com.tribune.backend.domain.dto.order.Order;
import com.tribune.backend.domain.dto.order.lineitem.product.Product;
import lombok.*;
import java.util.UUID;
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class LineItem {
private UUID id;
private Order order;
private Product product;
}

View File

@@ -1,16 +0,0 @@
package com.tribune.backend.domain.dto.order.lineitem.product;
import lombok.*;
import java.util.UUID;
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Product {
private UUID id;
}

View File

@@ -1,4 +1,4 @@
package com.tribune.backend.domain;
package com.tribune.backend.domain.element;
import lombok.experimental.SuperBuilder;

View File

@@ -0,0 +1,9 @@
package com.tribune.backend.domain.element;
/**
* Indicates any movable element that somehow relates to the domain model.
* Top-level marker interface for all domain element.
*/
public interface DomainObject {
}

View File

@@ -1,6 +1,7 @@
package com.tribune.backend.domain;
package com.tribune.backend.domain.element;
import com.tribune.backend.domain.error.DomainException;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.ArrayList;
@@ -12,10 +13,10 @@ import java.util.Objects;
* Abstract representation of Domain Entities
* @param <ID> - type of identifier (use wrapper on top of primitive types)
*/
@NoArgsConstructor
@SuperBuilder
public abstract class Entity<ID> implements IdentifiableDomainObject<ID> {
private final List<Object> domainEvents = new ArrayList<>();
private ID id;
@Override
@@ -23,6 +24,9 @@ public abstract class Entity<ID> implements IdentifiableDomainObject<ID> {
return id;
}
private final List<Object> domainEvents = new ArrayList<>();
public void setId(ID id) {
this.id = id;
}

View File

@@ -1,7 +1,7 @@
package com.tribune.backend.domain;
package com.tribune.backend.domain.element;
/**
* Interface for all domain objects that can be uniquely identified in some context
* Interface for all domain element that can be uniquely identified in some context
* @param <ID> - type of identifier (use wrapper on top of primitive types)
*/
public interface IdentifiableDomainObject<ID> extends DomainObject {

View File

@@ -1,4 +1,4 @@
package com.tribune.backend.domain;
package com.tribune.backend.domain.element;
import lombok.experimental.SuperBuilder;

View File

@@ -1,7 +1,7 @@
package com.tribune.backend.domain;
package com.tribune.backend.domain.element;
/**
* Marker interface for all value objects.
* Marker interface for all value element.
* Implementations of this interface are required to be immutable and
* implement equals() and hashCode().
*/

View File

@@ -0,0 +1,63 @@
package com.tribune.backend.domain.element.customer;
import com.tribune.backend.domain.element.Entity;
import com.tribune.backend.domain.element.customer.address.Address;
import com.tribune.backend.domain.element.customer.payment.Payment;
import lombok.*;
import lombok.experimental.SuperBuilder;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
@SuperBuilder
@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
public class Customer extends Entity<String> {
private String id;
private String displayName;
private String firstName;
private String lastName;
private CustomerState state;
private CustomerType type;
//todo: like Address
private List<Payment> paymentList;
private List<Address> addressList;
@Override
public String toString() {
return "Customer{" +
"id=" + id +
", displayName='" + displayName + '\'' +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", state=" + state +
", type=" + type +
", paymentList=" + paymentList +
", addressList=" + addressList +
'}';
}
}

View File

@@ -1,4 +1,4 @@
package com.tribune.backend.domain.dto.customer;
package com.tribune.backend.domain.element.customer;
public enum CustomerState {
CREATED,

View File

@@ -1,4 +1,4 @@
package com.tribune.backend.domain.dto.customer;
package com.tribune.backend.domain.element.customer;
public enum CustomerType {
TRADITIONAL,

View File

@@ -1,7 +1,7 @@
package com.tribune.backend.domain.dto.customer.address;
package com.tribune.backend.domain.element.customer.address;
import com.tribune.backend.domain.ValueObject;
import com.tribune.backend.domain.dto.customer.common.Country;
import com.tribune.backend.domain.element.ValueObject;
import com.tribune.backend.domain.element.customer.common.Country;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;

View File

@@ -1,4 +1,4 @@
package com.tribune.backend.domain.dto.customer.address;
package com.tribune.backend.domain.element.customer.address;
import lombok.NonNull;
import lombok.Value;

View File

@@ -1,4 +1,4 @@
package com.tribune.backend.domain.dto.customer.address;
package com.tribune.backend.domain.element.customer.address;
import lombok.NonNull;
import lombok.Value;

View File

@@ -1,6 +1,6 @@
package com.tribune.backend.domain.dto.customer.common;
package com.tribune.backend.domain.element.customer.common;
import com.tribune.backend.domain.ValueObject;
import com.tribune.backend.domain.element.ValueObject;
import lombok.NonNull;
import lombok.Value;

View File

@@ -1,4 +1,4 @@
package com.tribune.backend.domain.dto.customer.common;
package com.tribune.backend.domain.element.customer.common;
public enum Profession {

View File

@@ -0,0 +1,18 @@
package com.tribune.backend.domain.element.customer.invoice;
import com.tribune.backend.domain.element.customer.Customer;
import lombok.*;
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Invoice {
private Long id;
private Customer customer;
}

View File

@@ -1,20 +1,20 @@
package com.tribune.backend.domain.dto.customer.payment;
package com.tribune.backend.domain.element.customer.payment;
import com.tribune.backend.domain.LocalEntity;
import com.tribune.backend.domain.element.LocalEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.SuperBuilder;
import java.util.UUID;
@Data
@SuperBuilder
@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
public class Payment extends LocalEntity<UUID>{
public class Payment extends LocalEntity<Long>{
private String note;
private UUID customerId;
private Long customerId;
}

View File

@@ -0,0 +1,30 @@
package com.tribune.backend.domain.element.order;
import com.tribune.backend.domain.element.Entity;
import com.tribune.backend.domain.element.order.lineitem.LineItem;
import com.tribune.backend.domain.enums.OrderStatus;
import lombok.*;
import lombok.experimental.SuperBuilder;
import javax.validation.constraints.Size;
import java.math.BigDecimal;
import java.util.List;
@Setter
@Getter
@ToString
@SuperBuilder
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true)
public class Order extends Entity<Long> {
private Long id;
private OrderStatus status;
@Size(min = 1)
private List<LineItem> lineItems;
private BigDecimal payment;
}

View File

@@ -0,0 +1,21 @@
package com.tribune.backend.domain.element.order.lineitem;
import lombok.*;
@Setter
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class LineItem {
private Long id;
private Long order;
private Long product;
private Integer quantity;
}

View File

@@ -0,0 +1,25 @@
package com.tribune.backend.domain.element.order.lineitem.product;
import lombok.*;
import java.math.BigDecimal;
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Product {
private Long id;
private String name;
private String company;
private Integer quantity;
private BigDecimal price;
}

View File

@@ -1,5 +0,0 @@
package com.tribune.backend.domain.enums;
public enum BlogStatus {
CREATED,APPROVED,DECLINED,SETTLED;
}

View File

@@ -0,0 +1,8 @@
package com.tribune.backend.domain.error;
public class CustomerInvalidStateException extends RuntimeException{
public CustomerInvalidStateException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,8 @@
package com.tribune.backend.domain.error;
public class ProductNotAvailableException extends RuntimeException{
public ProductNotAvailableException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,8 @@
package com.tribune.backend.domain.error;
public class QuantityNotPermittedException extends RuntimeException{
public QuantityNotPermittedException(String message) {
super(message);
}
}

View File

@@ -1,13 +1,13 @@
package com.tribune.backend.domain.service;
import com.tribune.backend.domain.dto.customer.Customer;
import com.tribune.backend.domain.element.customer.Customer;
import java.util.List;
import java.util.UUID;
/**
* Please note that adapter implementation should NOT contain any business logic.
* All operations with Domain objects should be initiated and encapsulated only in domain module
* All operations with Domain element should be initiated and encapsulated only in domain module
*/
public interface CustomerRepositoryAdapter {

View File

@@ -0,0 +1,10 @@
package com.tribune.backend.domain.service;
import com.tribune.backend.domain.context.CustomerOrder;
import com.tribune.backend.domain.dto.SingleOrderResponse;
public interface PlaceOrderAdapter {
SingleOrderResponse validateOrder(CustomerOrder customerOrder);
}

View File

@@ -0,0 +1,83 @@
package com.tribune.backend.domain.service;
import com.tribune.backend.domain.context.CustomerOrder;
import com.tribune.backend.domain.dto.SingleOrderResponse;
import com.tribune.backend.domain.element.customer.Customer;
import com.tribune.backend.domain.element.customer.CustomerState;
import com.tribune.backend.domain.element.order.Order;
import com.tribune.backend.domain.element.order.lineitem.LineItem;
import com.tribune.backend.domain.element.order.lineitem.product.Product;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.math.BigDecimal;
import java.util.List;
@Slf4j
@Service
public class PlaceOrderAdapterImpl implements PlaceOrderAdapter {
//execute a business case
@Override
public SingleOrderResponse validateOrder(CustomerOrder customerOrder) {
log.info("validating CustomerOrder - {}", customerOrder);
List<Product> products = customerOrder.getProducts();
Order order = customerOrder.getOrder();
validateCustomer(customerOrder.getCustomer());
double totalCost = 0.0;
for (LineItem lineItem : order.getLineItems()) {
Product product = products.stream().filter(p -> p.getId().equals(lineItem.getProduct()))
.findFirst().orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST,
String.format("No product available with this id [%s].", lineItem.getProduct())));
//calculate costs
totalCost += product.getPrice().doubleValue() * lineItem.getQuantity();
//check for requested quantities
if (lineItem.getQuantity() <= product.getQuantity()) {
//subtract the quantity
product.setQuantity(product.getQuantity() - lineItem.getQuantity());
} else {
String error = String.format("Available quantity for product [%s] is %d, requested quantity was %d."
, product.getName(), product.getQuantity(), lineItem.getQuantity());
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, error);
}
}
//process payment
if (order.getPayment().doubleValue() < totalCost) {
String error = String.format("Total cost for order is %s, provided payment was %s."
, totalCost, order.getPayment());
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, error);
}
return SingleOrderResponse.builder()
.order(order)
.totalCost(BigDecimal.valueOf(totalCost))
.products(products)
.build();
}
private void validateCustomer(Customer customer) {
if (!customer.getState().equals(CustomerState.ACTIVE)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, String.format("Customer state invalid - %s", customer.getState()));
}
}
}

View File

@@ -58,28 +58,12 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Security dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>
</dependency>
<!-- Miscellaneous -->
<!-- Test dependencies -->
<dependency>
@@ -93,22 +77,6 @@
</exclusion>
</exclusions>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.security</groupId>-->
<!-- <artifactId>spring-security-test</artifactId>-->
<!-- <scope>test</scope>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>com.h2database</groupId>-->
<!-- <artifactId>h2</artifactId>-->
<!-- <version>2.1.212</version>-->
<!-- <scope>test</scope>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.testcontainers</groupId>-->
<!-- <artifactId>postgresql</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>${project.groupId}</groupId>

View File

@@ -1,80 +0,0 @@
package com.tribune.backend.infrastructure.config;
import com.tribune.backend.domain.dto.GenericResponse;
import com.tribune.backend.infrastructure.config.security.SecurityConfig;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static javax.ws.rs.core.Response.Status.Family.SUCCESSFUL;
/**
* top tier config file : a config for configs
* can log responses as following:
*
* ObjectMapper mapper=new ObjectMapper();
* mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS,false);
*
* ... mapper.writeValueAsString(value)
**/
@Configuration
public class AppConfig {
/**
* Fix cycle dependency formed by {@link org.keycloak.adapters.KeycloakConfigResolver}
* if specified within {@link SecurityConfig}
**/
@Bean
public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> {
JsonSerializer<Response> jsonSerializer = new JsonSerializer<>() {
@Override
public void serialize(Response value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
String message = value.getStatusInfo().getFamily().equals(SUCCESSFUL) ? "Success" : "Error";
List<String> reason = new ArrayList<>();
Object data = null;
switch (value.getStatusInfo().getStatusCode()) {
case 409:
reason.add("Email/username already exists!");
break;
case 201:
//returns as a result of newly generated resources
//returns a URL that ends with the newly created resource id
//"http://localhost:8080/auth/admin/realms/tribune/users/b61adfce-ffe6-4d68-9169-0c476ae11179"
data = value.getLocation();
break;
}
reason.add(value.getStatusInfo().getReasonPhrase());
GenericResponse<?> response = GenericResponse.builder()
.data(data)
.code(value.getStatus())
.message(message)
.reason(reason)
.build();
gen.writePOJO(response);
}
};
builder.serializerByType(Response.class, jsonSerializer);
};
}
}

View File

@@ -1,30 +0,0 @@
package com.tribune.backend.infrastructure.config.security;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.Collections;
import java.util.List;
@Configuration
public class CorsConfig {
// @Value("${app.cors.allowed-origins}")
// private List<String> allowedOrigins;
//
// @Bean
// CorsFilter corsFilter() {
// UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// CorsConfiguration config = new CorsConfiguration();
// config.setAllowCredentials(true);
// config.setAllowedOrigins(allowedOrigins);
// config.setAllowedMethods(Collections.singletonList("*"));
// config.setAllowedHeaders(Collections.singletonList("*"));
// source.registerCorsConfiguration("/**", config);
// return new CorsFilter(source);
// }
}

View File

@@ -1,16 +0,0 @@
package com.tribune.backend.infrastructure.config.security;
import org.keycloak.representations.idm.CredentialRepresentation;
public class CredentialsUtils {
private CredentialsUtils(){}
public static CredentialRepresentation createPasswordCredentials(String password) {
CredentialRepresentation passwordCredentials = new CredentialRepresentation();
passwordCredentials.setTemporary(false);
passwordCredentials.setType(CredentialRepresentation.PASSWORD);
passwordCredentials.setValue(password);
return passwordCredentials;
}
}

View File

@@ -1,21 +0,0 @@
package com.tribune.backend.infrastructure.config.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.*;
import java.io.IOException;
@Slf4j
public class CustomFilter extends GenericFilterBean {
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
log.error("XXX");
chain.doFilter(request, response);
}
}

View File

@@ -1,45 +0,0 @@
package com.tribune.backend.infrastructure.config.security;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.KeycloakBuilder;
import org.keycloak.admin.client.resource.UsersResource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Slf4j
@Getter
@Setter
@ConfigurationProperties(prefix = "tribune.keycloak.client.user-management")
public class KeycloakConfigProperties {
@Value("${keycloak.auth-server-url}")
private String authServerUrl;
@Value("${keycloak.realm}")
private String realm;
private String resource;
private String clientSecret;
private String username;
private String password;
public UsersResource getInstance(){
ResteasyClient resteasyClient=new ResteasyClientBuilder()
.connectionPoolSize(10)
.build();
return KeycloakBuilder.builder()
.serverUrl(authServerUrl)
.realm(realm)
.grantType(OAuth2Constants.PASSWORD)
.username(username)
.password(password)
.clientId(resource)
.clientSecret(clientSecret)
.resteasyClient(resteasyClient)
.build().realm(realm).users();
}
}

View File

@@ -1,94 +0,0 @@
package com.tribune.backend.infrastructure.config.security;
import lombok.extern.slf4j.Slf4j;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
/**
* The {@link EnableGlobalMethodSecurity} enables direct config of role-based access
* to our controllers. See the following example:
* <pre>{@code
* @PreAuthorize("hasRole('USER')")
* @GetMapping("/title)
* public ResponseEntity<String>getTitle(){
* //
* return title;
* }
* }</pre>
*/
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@KeycloakConfiguration
//@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
/**
* The other way around
* return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
* */
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
// Enable CORS and disable CSRF
http
.cors()
.and()
.csrf().disable()
// Set session management to stateless
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/v1/transaction/status/**").hasRole(MERCHANT)
.antMatchers(HttpMethod.POST, "/v1/transaction/**").hasRole(MERCHANT)
.antMatchers(HttpMethod.POST, "/v1/users/**").anonymous()
.antMatchers(HttpMethod.GET, "/api/user-data/me", "/actuator/**").permitAll()
.antMatchers(HttpMethod.GET, "/v1/dummy/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs", "/v3/api-docs/**").permitAll()
.anyRequest().fullyAuthenticated();
}
/**
* provided by keycloak to handle authentication failures.
* */
@Bean
@Override
protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
KeycloakAuthenticationProcessingFilter filter = new KeycloakAuthenticationProcessingFilter(this.authenticationManagerBean());
filter.setSessionAuthenticationStrategy(this.sessionAuthenticationStrategy());
filter.setAuthenticationFailureHandler(new TribuneKeycloakAuthenticationFailureHandler());
return filter;
}
public static final String MERCHANT = "merchant";
public static final String USER = "user";
public static final String MANAGE_USERS = "manage-users";
}

View File

@@ -1,36 +0,0 @@
package com.tribune.backend.infrastructure.config.security;
import com.tribune.backend.domain.dto.GenericResponse;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static org.keycloak.util.JsonSerialization.mapper;
@Slf4j
@AllArgsConstructor
public class TribuneKeycloakAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex) throws IOException {
ex.printStackTrace();
log.error("Authentication Error: {}", ex.getMessage());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
//todo:check removal of WWW_AUTHENTICATE header from response
GenericResponse<Void> genericResponse = GenericResponse.<Void>builder()
.message(ex.getMessage())
.reason(response.getHeader(HttpHeaders.WWW_AUTHENTICATE))
.code(response.getStatus())
.build();
response.getWriter().write(mapper.writeValueAsString(genericResponse));
}
}

View File

@@ -1,24 +0,0 @@
package com.tribune.backend.infrastructure.config.security;
import lombok.*;
import javax.validation.constraints.NotBlank;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserDTO {
@NotBlank
private String username;
@NotBlank
private String email;
@NotBlank
private String password;
@NotBlank
private String firstName;
@NotBlank
private String lastName;
}

View File

@@ -1,21 +0,0 @@
package com.tribune.backend.infrastructure.config.security;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
@Getter
@Setter
@AllArgsConstructor
@Builder
public class UserLoginRequest {
@NotBlank
private String username;
@NotBlank
private String password;
}

View File

@@ -1,25 +0,0 @@
package com.tribune.backend.infrastructure.config.security;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.tribune.backend.domain.dto.GenericResponse;
import org.keycloak.representations.idm.UserRepresentation;
import javax.ws.rs.core.Response;
import java.util.List;
public interface UserManagementService {
Response addUser(UserDTO userDTO);
List<UserRepresentation> getUser(String userName);
void updateUser(String userId, UserDTO userDTO);
void deleteUser(String userId);
void sendVerificationLink(String userId);
void sendResetPassword(String userId);
GenericResponse<ObjectNode> authenticate(UserLoginRequest userDTO);
}

View File

@@ -1,139 +0,0 @@
package com.tribune.backend.infrastructure.config.security;
import com.tribune.backend.domain.dto.GenericResponse;
import com.tribune.backend.infrastructure.error.BackendException;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.netty.http.client.HttpClient;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
@Slf4j
@Service
public class UserManagementServiceImpl implements UserManagementService {
@Value("${keycloak.resource}")
private String tribuneSpringbootClient;
private final KeycloakConfigProperties properties;
@Autowired
public UserManagementServiceImpl(KeycloakConfigProperties properties) {
this.properties = properties;
}
@Override
public Response addUser(UserDTO userDTO) {
CredentialRepresentation credential = CredentialsUtils
.createPasswordCredentials(userDTO.getPassword());
UserRepresentation user = new UserRepresentation();
user.setUsername(userDTO.getUsername());
user.setFirstName(userDTO.getFirstName());
user.setLastName(userDTO.getLastName());
user.setEmail(userDTO.getEmail());
user.setCredentials(Collections.singletonList(credential));
user.setEnabled(true);
return properties.getInstance().create(user);
}
@Override
public List<UserRepresentation> getUser(String userName) {
return properties.getInstance().search(userName, true);
}
@Override
public void updateUser(String userId, UserDTO userDTO) {
CredentialRepresentation credential = CredentialsUtils
.createPasswordCredentials(userDTO.getPassword());
UserRepresentation user = new UserRepresentation();
user.setUsername(userDTO.getUsername());
user.setFirstName(userDTO.getFirstName());
user.setLastName(userDTO.getLastName());
user.setEmail(userDTO.getEmail());
user.setCredentials(Collections.singletonList(credential));
//todo:needs confirmation
properties.getInstance().get(userId).update(user);
}
@Override
public void deleteUser(String userId) {
//todo:needs confirmation
properties.getInstance().get(userId)
.remove();
}
@Override
public void sendVerificationLink(String userId) {
//todo:needs confirmation
properties.getInstance().get(userId)
.sendVerifyEmail();
}
@Override
public void sendResetPassword(String userId) {
//todo:needs confirmation
properties.getInstance().get(userId)
.executeActionsEmail(List.of("UPDATE_PASSWORD"));
}
@Override
public GenericResponse<ObjectNode> authenticate(UserLoginRequest userDTO) {
URI uri=UriComponentsBuilder.fromUriString(
String.format("%s/realms/%s/protocol/openid-connect/token",
properties.getAuthServerUrl(), properties.getRealm())
).build().toUri();
HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("username", userDTO.getUsername());
formData.add("password", userDTO.getPassword());
formData.add("client_id", tribuneSpringbootClient);
formData.add("grant_type", CredentialRepresentation.PASSWORD);
ObjectNode node= WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build()
.post()
.uri(uri)
.body(BodyInserters.fromFormData(formData))
.accept(MediaType.APPLICATION_JSON)
.exchangeToMono(clientResponse -> {
if (clientResponse.statusCode().is4xxClientError()) {
log.error("error");
return clientResponse.bodyToMono(ObjectNode.class).map(val -> {
throw new BackendException(val, clientResponse.statusCode());
});
} else
return clientResponse.bodyToMono(ObjectNode.class);
})
.block();
return GenericResponse.<ObjectNode>builder()
.code(200)
.message("Success!")
.data(node)
.build();
}
}

View File

@@ -1,8 +1,9 @@
package com.tribune.backend.infrastructure.controller;
import com.tribune.backend.domain.dto.PlaceOrderRequest;
import com.tribune.backend.domain.dto.SubmitOrderRequest;
import com.tribune.backend.domain.dto.GenericResponse;
import com.tribune.backend.domain.dto.SingleOrderResponse;
import com.tribune.backend.infrastructure.error.NotFoundException;
import com.tribune.backend.infrastructure.services.OrderService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -18,13 +19,17 @@ public class OrdersController {
private final OrderService orderService;
@PostMapping("/order")
public GenericResponse<SingleOrderResponse> createBlog(@Valid @RequestBody PlaceOrderRequest createOrderRequest) {
@PostMapping("/submitOrder")
public GenericResponse<SingleOrderResponse> placeOrder(@Valid @RequestBody SubmitOrderRequest createOrderRequest) throws NotFoundException {
SingleOrderResponse orderResponse=orderService.processOrder(createOrderRequest);
return GenericResponse.<SingleOrderResponse>builder()
.code(201)
.data(orderService.placeOrder(createOrderRequest))
.data(orderResponse)
.message("Created!")
.build();
}
}

View File

@@ -1,64 +0,0 @@
package com.tribune.backend.infrastructure.controller;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.tribune.backend.domain.dto.GenericResponse;
import com.tribune.backend.infrastructure.config.security.UserDTO;
import com.tribune.backend.infrastructure.config.security.UserLoginRequest;
import com.tribune.backend.infrastructure.config.security.UserManagementService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.ws.rs.core.Response;
import java.util.List;
@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping(path = "/v1/users")
public class UserController {
private final UserManagementService service;
@PostMapping("/user/auth")
public GenericResponse<ObjectNode> authenticate(@Valid @RequestBody UserLoginRequest userDTO){
return service.authenticate(userDTO);
}
@PostMapping("/user")
public Response addUser(@Valid @RequestBody UserDTO userDTO){
return service.addUser(userDTO);
}
@GetMapping(path = "/user/{userName}")
public List<UserRepresentation> getUser(@PathVariable("userName") String userName){
return service.getUser(userName);
}
@PutMapping(path = "/user/{userId}")
public String updateUser(@PathVariable("userId") String userId, @RequestBody UserDTO userDTO){
service.updateUser(userId, userDTO);
return "User Details Updated Successfully.";
}
@DeleteMapping(path = "/user/{userId}")
public String deleteUser(@PathVariable("userId") String userId){
service.deleteUser(userId);
return "User Deleted Successfully.";
}
@GetMapping(path = "/user/verify/{userId}")
public String sendVerificationLink(@PathVariable("userId") String userId){
service.sendVerificationLink(userId);
return "Verification Link Send to Registered E-mail Id.";
}
@GetMapping(path = "/user/reset-password/{userId}")
public String sendResetPasswordLink(@PathVariable("userId") String userId){
service.sendResetPassword(userId);
return "Reset Password Link Send Successfully to Registered E-mail Id.";
}
}

View File

@@ -0,0 +1,87 @@
package com.tribune.backend.infrastructure.db;
import com.tribune.backend.domain.element.customer.CustomerState;
import com.tribune.backend.domain.element.customer.CustomerType;
import com.tribune.backend.infrastructure.db.entities.CustomerEntity;
import com.tribune.backend.infrastructure.db.entities.ProductEntity;
import com.tribune.backend.infrastructure.db.repository.CustomerRepository;
import com.tribune.backend.infrastructure.db.repository.ProductRepository;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
@Slf4j
@Transactional
@AllArgsConstructor
@Component
public class DevBootstrap implements CommandLineRunner {
private CustomerRepository customerRepository;
private ProductRepository productRepository;
@Override
public void run(String... args) {
initCustomers();
initProducts();
}
public void initCustomers() {
customerRepository.deleteAll();
CustomerEntity customer0 = CustomerEntity.builder()
.firstName("John")
.lastName("Doe")
.type(CustomerType.TRADITIONAL)
.displayName("Failure")
.state(CustomerState.ACTIVE)
.build();
CustomerEntity customer1 = CustomerEntity.builder()
.firstName("Bill")
.lastName("Gates")
.type(CustomerType.TRADITIONAL)
.displayName("Billy the beast")
.state(CustomerState.DISABLED)
.build();
customerRepository.saveAll(List.of(customer0, customer1));
}
public void initProducts() {
productRepository.deleteAll();
ProductEntity product0 = ProductEntity.builder()
.name("Samsung Galaxy S23")
.company("Samsung")
.price(BigDecimal.valueOf(3112.50))
.quantity(15)
.build();
ProductEntity product1 = ProductEntity.builder()
.name("Iphone 14 pro max")
.company("Apple")
.price(BigDecimal.valueOf(2000))
.quantity(5)
.build();
ProductEntity product2 = ProductEntity.builder()
.name("Xiaomi Note 11")
.company("Xiaomi")
.price(BigDecimal.valueOf(800))
.quantity(22)
.build();
ProductEntity product3 = ProductEntity.builder()
.name("Nokia whatever")
.company("Nokia")
.price(BigDecimal.valueOf(1500))
.quantity(82)
.build();
productRepository.saveAll(List.of(product0, product1, product2, product3));
}
}

View File

@@ -2,11 +2,11 @@ package com.tribune.backend.infrastructure.db.entities;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.UUID;
@@ -16,13 +16,21 @@ import java.util.UUID;
@AllArgsConstructor
@Builder
@Entity
@Table(name ="ADDRESS")
public class AddressEntity {
@Id
private UUID id;
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "CUSTOMER")
private UUID customer;
@ManyToOne
@JoinColumn(name = "customer",referencedColumnName = "id")
private CustomerEntity customerEntity;
@CreationTimestamp
@Column(name = "CREATION_TIMESTAMP",nullable = false,updatable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private LocalDateTime creationTimestamp;
@UpdateTimestamp
@Column(name = "UPDATE_TIMESTAMP",nullable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private LocalDateTime updateTimestamp;
}

View File

@@ -1,13 +1,15 @@
package com.tribune.backend.infrastructure.db.entities;
import com.tribune.backend.domain.dto.customer.CustomerState;
import com.tribune.backend.domain.dto.customer.CustomerType;
import com.tribune.backend.domain.element.customer.CustomerState;
import com.tribune.backend.domain.element.customer.CustomerType;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
@Setter
@Getter
@@ -15,10 +17,12 @@ import java.util.UUID;
@AllArgsConstructor
@Builder
@Entity
@Table(name ="CUSTOMER")
public class CustomerEntity {
@Id
private UUID id;
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String displayName;
@@ -32,9 +36,20 @@ public class CustomerEntity {
@Enumerated(value = EnumType.STRING)
private CustomerType type;
@OneToMany(mappedBy = "customerEntity", fetch = FetchType.LAZY)
@OneToMany(mappedBy = "customer", fetch = FetchType.LAZY)
private List<PaymentEntity> paymentEntityList;
@OneToMany(mappedBy = "customerEntity", fetch = FetchType.LAZY)
@OneToMany(mappedBy = "customer", fetch = FetchType.LAZY)
private List<AddressEntity> addressEntityList;
@OneToMany(mappedBy = "customer", fetch = FetchType.LAZY)
private List<InvoiceEntity> invoiceEntityList;
@CreationTimestamp
@Column(name = "CREATION_TIMESTAMP",nullable = false,updatable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private LocalDateTime creationTimestamp;
@UpdateTimestamp
@Column(name = "UPDATE_TIMESTAMP",nullable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private LocalDateTime updateTimestamp;
}

View File

@@ -2,10 +2,11 @@ package com.tribune.backend.infrastructure.db.entities;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.UUID;
@Setter
@@ -14,13 +15,23 @@ import java.util.UUID;
@AllArgsConstructor
@Builder
@Entity
@Table(name = "INVOICE")
public class InvoiceEntity {
@Id
private UUID id;
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
private CustomerEntity customerEntity;
@Column(name = "CUSTOMER")
private UUID customer;
@CreationTimestamp
@Column(name = "CREATION_TIMESTAMP", nullable = false, updatable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private LocalDateTime creationTimestamp;
@UpdateTimestamp
@Column(name = "UPDATE_TIMESTAMP", nullable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private LocalDateTime updateTimestamp;
}

View File

@@ -12,14 +12,21 @@ import java.util.UUID;
@AllArgsConstructor
@Builder
@Entity
@Table(name ="LINE_ITEM")
public class LineItemEntity {
@Id
private UUID id;
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "customer_order")
private Long customerOrder;
@JoinColumn(name = "product",referencedColumnName = "id")
@ManyToOne
private OrderEntity orderEntity;
private ProductEntity product;
@OneToOne
private ProductEntity productEntity;
private Integer quantity;
}

View File

@@ -3,26 +3,44 @@ package com.tribune.backend.infrastructure.db.entities;
import com.tribune.backend.domain.enums.OrderStatus;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@DynamicUpdate
@Entity
@Table(name = "CUSTOMER_ORDER")
public class OrderEntity {
@Id
private UUID id;
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(value = EnumType.STRING)
private OrderStatus status;
@OneToMany
private List<LineItemEntity> lineItemEntities;
@OneToMany(mappedBy = "id")
private List<LineItemEntity> lineItems;
private BigDecimal payment;
@CreationTimestamp
@Column(name = "CREATION_TIMESTAMP",nullable = false,updatable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private LocalDateTime creationTimestamp;
@UpdateTimestamp
@Column(name = "UPDATE_TIMESTAMP",nullable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private LocalDateTime updateTimestamp;
}

View File

@@ -2,11 +2,11 @@ package com.tribune.backend.infrastructure.db.entities;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.UUID;
@Setter
@@ -15,13 +15,20 @@ import java.util.UUID;
@AllArgsConstructor
@Builder
@Entity
@Table(name ="PAYMENT")
public class PaymentEntity {
@Id
private UUID id;
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "CUSTOMER")
private UUID customer;
@CreationTimestamp
@Column(name = "CREATION_TIMESTAMP",nullable = false,updatable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private LocalDateTime creationTimestamp;
@ManyToOne
@JoinColumn(name = "customer",referencedColumnName = "id")
private CustomerEntity customerEntity;
@UpdateTimestamp
@Column(name = "UPDATE_TIMESTAMP",nullable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private LocalDateTime updateTimestamp;
}

View File

@@ -2,19 +2,42 @@ package com.tribune.backend.infrastructure.db.entities;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.Entity;
import javax.persistence.Id;
import java.util.UUID;
import javax.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Setter
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
@DynamicUpdate
@Entity
@Table(name ="PRODUCT")
public class ProductEntity {
@Id
private UUID id;
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String company;
private Integer quantity;
private BigDecimal price;
@CreationTimestamp
@Column(name = "CREATION_TIMESTAMP",nullable = false,updatable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private LocalDateTime creationTimestamp;
@UpdateTimestamp
@Column(name = "UPDATE_TIMESTAMP",nullable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private LocalDateTime updateTimestamp;
}

View File

@@ -5,10 +5,8 @@ import com.tribune.backend.infrastructure.db.entities.CustomerEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.UUID;
@Repository
public interface CustomerRepository extends JpaRepository<CustomerEntity, UUID> {
public interface CustomerRepository extends JpaRepository<CustomerEntity, Long> {
}

View File

@@ -0,0 +1,15 @@
package com.tribune.backend.infrastructure.db.repository;
import com.tribune.backend.infrastructure.db.entities.ProductEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ProductRepository extends JpaRepository<ProductEntity, Long> {
List<ProductEntity> findByIdIn(List<Long> ids);
}

View File

@@ -0,0 +1,71 @@
package com.tribune.backend.infrastructure.error;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.tribune.backend.domain.dto.GenericResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@Slf4j
@RestControllerAdvice
public class ControllerAdvisor extends ResponseEntityExceptionHandler {
@ExceptionHandler(BackendException.class)
public ResponseEntity<GenericResponse<ObjectNode>> handleException(BackendException e) {
log.error("status:{} --- error: {}", e.getStatus(), e.getError());
GenericResponse<ObjectNode> response = GenericResponse.<ObjectNode>builder()
.message(e.getMessage())
.code(e.getStatus().value())
.data(e.getError())
.build();
return ResponseEntity.status(e.getStatus()).body(response);
}
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<GenericResponse<ObjectNode>> handleException(ResponseStatusException e) {
log.error("status:{} --- error: {}", e.getStatus(), e.getMessage());
GenericResponse<ObjectNode> response = GenericResponse.<ObjectNode>builder()
.message(e.getReason())
.code(e.getStatus().value())
.build();
return ResponseEntity.status(e.getStatus()).body(response);
}
@Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException e, HttpHeaders headers, HttpStatus status, WebRequest request) {
GenericResponse<ObjectNode> response = GenericResponse.<ObjectNode>builder()
.message(e.getMessage())
.code(HttpStatus.BAD_REQUEST.value())
.build();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException e, HttpHeaders headers, HttpStatus status, WebRequest request) {
String message = "Error occurred!";
FieldError fieldError = e.getFieldError();
if (fieldError != null) {
message = String.format("[%s] %s",fieldError.getField(),fieldError.getDefaultMessage());
}
GenericResponse<ObjectNode> response = GenericResponse.<ObjectNode>builder()
.message(message)
.code(HttpStatus.BAD_REQUEST.value())
.build();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
}

View File

@@ -1,27 +0,0 @@
package com.tribune.backend.infrastructure.error;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.tribune.backend.domain.dto.GenericResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@Slf4j
@RestControllerAdvice
public class TribuneAdvisor extends ResponseEntityExceptionHandler {
@ExceptionHandler(BackendException.class)
public ResponseEntity<GenericResponse<ObjectNode>> handleException(BackendException e) {
log.error("status:{} --- error: {}",e.getStatus(),e.getError());
GenericResponse<ObjectNode> response=GenericResponse.<ObjectNode>builder()
.message(e.getMessage())
.code(e.getStatus().value())
.data(e.getError())
.build();
return ResponseEntity.status(e.getStatus()).body(response);
}
}

View File

@@ -0,0 +1,14 @@
package com.tribune.backend.infrastructure.mappers;
import com.tribune.backend.domain.element.customer.Customer;
import com.tribune.backend.infrastructure.db.entities.CustomerEntity;
import org.mapstruct.Mapper;
@Mapper(componentModel = "spring")
public interface CustomerMapper {
Customer toCustomer(CustomerEntity entity);
CustomerEntity toCustomerEntity(Customer customer);
}

View File

@@ -0,0 +1,20 @@
package com.tribune.backend.infrastructure.mappers;
import com.tribune.backend.domain.element.order.lineitem.LineItem;
import com.tribune.backend.infrastructure.db.entities.LineItemEntity;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
public interface LineItemMapper {
@Mapping(source = "product.id", target = "product")
@Mapping(source = "customerOrder", target = "order")
LineItem toLineItem(LineItemEntity entity);
@Mapping(source = "product", target = "product.id")
@Mapping(source = "order", target = "customerOrder")
LineItemEntity toLineItemEntity(LineItem lineItem);
}

View File

@@ -0,0 +1,18 @@
package com.tribune.backend.infrastructure.mappers;
import com.tribune.backend.domain.element.order.Order;
import com.tribune.backend.infrastructure.db.entities.OrderEntity;
import org.mapstruct.Mapper;
@Mapper(componentModel = "spring", uses = LineItemMapper.class)
public interface OrderMapper {
Order toOrder(OrderEntity entity);
OrderEntity toOrderEntity(Order order);
}

View File

@@ -0,0 +1,14 @@
package com.tribune.backend.infrastructure.mappers;
import com.tribune.backend.domain.element.order.lineitem.product.Product;
import com.tribune.backend.infrastructure.db.entities.ProductEntity;
import org.mapstruct.Mapper;
@Mapper(componentModel = "spring")
public interface ProductMapper {
Product toProduct(ProductEntity entity);
ProductEntity toProductEntity(Product product);
}

View File

@@ -3,10 +3,12 @@ package com.tribune.backend.infrastructure.services;
import com.tribune.backend.domain.dto.*;
import com.tribune.backend.infrastructure.db.entities.OrderEntity;
import com.tribune.backend.infrastructure.error.NotFoundException;
public interface OrderService {
OrderEntity getById(String id);
SingleOrderResponse placeOrder(PlaceOrderRequest placeOrderRequest);
SingleOrderResponse processOrder(SubmitOrderRequest submitOrderRequest) throws NotFoundException;
OrderEntity updateOrder(String id, UpdateOrderRequest updateOrderRequest);

View File

@@ -1,15 +1,49 @@
package com.tribune.backend.infrastructure.services;
import com.tribune.backend.domain.context.CustomerOrder;
import com.tribune.backend.domain.dto.*;
import com.tribune.backend.domain.element.customer.Customer;
import com.tribune.backend.domain.element.order.lineitem.LineItem;
import com.tribune.backend.domain.enums.OrderStatus;
import com.tribune.backend.domain.service.PlaceOrderAdapter;
import com.tribune.backend.infrastructure.db.entities.CustomerEntity;
import com.tribune.backend.infrastructure.db.entities.OrderEntity;
import com.tribune.backend.infrastructure.db.entities.ProductEntity;
import com.tribune.backend.infrastructure.db.repository.CustomerRepository;
import com.tribune.backend.infrastructure.db.repository.OrderRepository;
import com.tribune.backend.infrastructure.db.repository.ProductRepository;
import com.tribune.backend.infrastructure.error.NotFoundException;
import com.tribune.backend.infrastructure.mappers.CustomerMapperImpl;
import com.tribune.backend.infrastructure.mappers.OrderMapperImpl;
import com.tribune.backend.infrastructure.mappers.ProductMapperImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@RequiredArgsConstructor
@Service
public class OrderServiceImpl implements OrderService {
private final CustomerRepository customerRepository;
private final ProductRepository productRepository;
private final OrderRepository orderRepository;
private final PlaceOrderAdapter adapter;
private final CustomerMapperImpl customerMapper;
private final OrderMapperImpl orderMapper;
private final ProductMapperImpl productMapper;
@Override
public OrderEntity getById(String id) {
@@ -17,10 +51,61 @@ public class OrderServiceImpl implements OrderService {
}
@Override
public SingleOrderResponse placeOrder(PlaceOrderRequest placeOrderRequest) {
return null;
public SingleOrderResponse processOrder(SubmitOrderRequest submitOrderRequest) throws NotFoundException {
log.info("Process order - {}", submitOrderRequest);
// now I'm on infrastructure
// I need to execute a business case
// Business is executed on domain
// how will I execute this business on domain?
// I need to obtain info about this business case and transform it to domain element encapsulated within a bounded context
// then be transferred to the domain
CustomerEntity customerEntity = customerRepository.findById(submitOrderRequest.getUser())
.orElseThrow(() -> new NotFoundException("No customer was found by the given id"));
// same with product list
List<Long> ids = submitOrderRequest.getOrder().getLineItems()
.stream().map(LineItem::getProduct).collect(Collectors.toList());
log.info("product ids:{}", ids);
List<ProductEntity> productEntities = productRepository.findByIdIn(ids);
log.info("products - {}", productEntities);
Customer customer = customerMapper.toCustomer(customerEntity);
OrderEntity orderEntity = orderMapper.toOrderEntity(submitOrderRequest.getOrder());
orderEntity.setStatus(OrderStatus.INITIALIZED);
orderRepository.saveAndFlush(orderEntity);
//map the customer and the order entity to domain element
//this way we provide input verification ___here's the result after persisting to db
CustomerOrder customerOrder = CustomerOrder.builder()
.customer(customer)
.order(orderMapper.toOrder(orderEntity))
.products(productEntities.stream().map(productMapper::toProduct).collect(Collectors.toList()))
.build();
//after obtaining all info here now it's time to move them to domain to be executed
SingleOrderResponse response = adapter.validateOrder(customerOrder);
//persist quantities
productRepository.saveAll(response.getProducts().stream().map(productMapper::toProductEntity).collect(Collectors.toList()));
orderEntity.setStatus(OrderStatus.SUBMITTED);
orderRepository.save(orderEntity);
return response;
}
//here we can update the order status case success/failure of business
@Override
public OrderEntity updateOrder(String id, UpdateOrderRequest updateOrderRequest) {
return null;

46
pom.xml
View File

@@ -6,7 +6,6 @@
<modules>
<module>backend-domain</module>
<module>backend-api-definition</module>
<module>backend-infrastructure</module>
<module>backend-application</module>
</modules>
@@ -19,12 +18,17 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<spring-boot-version>2.7.0</spring-boot-version>
<spring-boot-version>2.7.9</spring-boot-version>
<maven-jar-plugin.version>3.2.0</maven-jar-plugin.version>
<maven-compiler-plugin.version>3.9.0</maven-compiler-plugin.version>
<apache.commons.validator.version>1.7</apache.commons.validator.version>
<keycloak-version>18.0.0</keycloak-version>
<mapstruct.version>1.5.3.Final</mapstruct.version>
<lombok.version>1.18.12</lombok.version>
<dockerfile.tag>${project.version}</dockerfile.tag>
@@ -36,10 +40,14 @@
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>compile</scope>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
@@ -74,17 +82,6 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Security dependencies-->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>${keycloak-version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>
<version>${keycloak-version}</version>
</dependency>
<!-- Test dependencies -->
<dependency>
@@ -104,13 +101,22 @@
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.9.0</version>
<version>${maven-compiler-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
@@ -156,7 +162,7 @@
<image>gcr.io/distroless/java:11</image>
</from>
<to>
<image>tribune.jfrog.io/tribune/tribune-blogs:${project.version}</image>
<image>tribune.jfrog.io/tribune/ddd-initiative:${project.version}</image>
</to>
</configuration>
</plugin>