diff --git a/.github/workflows/ci-code-quality.yml b/.github/workflows/ci-code-quality.yml
new file mode 100644
index 0000000..522ff77
--- /dev/null
+++ b/.github/workflows/ci-code-quality.yml
@@ -0,0 +1,22 @@
+name: Qodana
+on:
+ workflow_dispatch:
+ pull_request:
+ push:
+ branches:
+ - main
+
+jobs:
+ qodana:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+ checks: write
+ steps:
+ - name: Checkout latest code
+ uses: actions/checkout@v4
+ - name: 'Qodana Scan'
+ uses: JetBrains/qodana-action@v2024.1
+ env:
+ QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
\ No newline at end of file
diff --git a/pta-common/pom.xml b/pta-common/pom.xml
index f63e097..869a28f 100644
--- a/pta-common/pom.xml
+++ b/pta-common/pom.xml
@@ -10,9 +10,13 @@
../pta-parent/pom.xml
pta-common
- 0.3.0
+ 0.4.0
jar
+
+ io.quarkus
+ quarkus-rest
+
io.smallrye.reactive
mutiny
diff --git a/pta-common/src/main/java/ch/obya/pta/common/domain/repository/EntityRepository.java b/pta-common/src/main/java/ch/obya/pta/common/domain/repository/EntityRepository.java
index caefa1e..210a370 100644
--- a/pta-common/src/main/java/ch/obya/pta/common/domain/repository/EntityRepository.java
+++ b/pta-common/src/main/java/ch/obya/pta/common/domain/repository/EntityRepository.java
@@ -1,27 +1,24 @@
package ch.obya.pta.common.domain.repository;
-import java.util.Collection;
-
-import io.smallrye.mutiny.Multi;
-import io.smallrye.mutiny.Uni;
-
import ch.obya.pta.common.domain.entity.Entity;
import ch.obya.pta.common.domain.vo.Identity;
-import ch.obya.pta.common.util.search.FindCriteria;
+import ch.obya.pta.common.util.search.AttributeFilter;
+import io.smallrye.mutiny.Uni;
+
+import java.util.List;
+import java.util.Map;
public interface EntityRepository, I extends Identity, S> {
Uni findOne(I id);
- Multi findByCriteria(Collection criteria);
+ Uni> findByCriteria(Map> criteria);
- default Multi findAll() {
- return findByCriteria(FindCriteria.empty());
- }
+ Uni> findAll();
- Uni save(I id, S state);
+ Uni save(I id, S state);
- default Uni save(Entity entity) {
+ default Uni save(Entity entity) {
return save(entity.id(), entity.state());
}
diff --git a/pta-common/src/main/java/ch/obya/pta/common/domain/repository/InMemoryKeyValueRepository.java b/pta-common/src/main/java/ch/obya/pta/common/domain/repository/InMemoryKeyValueRepository.java
index 03ec775..1fed5f0 100644
--- a/pta-common/src/main/java/ch/obya/pta/common/domain/repository/InMemoryKeyValueRepository.java
+++ b/pta-common/src/main/java/ch/obya/pta/common/domain/repository/InMemoryKeyValueRepository.java
@@ -1,21 +1,20 @@
package ch.obya.pta.common.domain.repository;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.function.BiFunction;
-
-import io.smallrye.mutiny.Multi;
-import io.smallrye.mutiny.Uni;
-
import ch.obya.pta.common.domain.entity.BaseEntity;
import ch.obya.pta.common.domain.entity.Entity;
import ch.obya.pta.common.domain.vo.Identity;
-import ch.obya.pta.common.util.search.FindCriteria;
+import ch.obya.pta.common.util.search.AttributeFilter;
+import io.smallrye.mutiny.Uni;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.experimental.FieldDefaults;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+
import static java.util.stream.Collectors.toList;
@NoArgsConstructor
@@ -36,14 +35,19 @@ public Uni findOne(I id) {
}
@Override
- public Multi findByCriteria(Collection criteria) {
- return Multi.createFrom().iterable(store.entrySet().stream().map(e -> entityCreator.apply(e.getKey(), e.getValue())).collect(toList()));
+ public Uni> findByCriteria(Map> criteria) {
+ return findAll();
+ }
+
+ @Override
+ public Uni> findAll() {
+ return Uni.createFrom().item(store.entrySet().stream().map(e -> entityCreator.apply(e.getKey(), e.getValue())).collect(toList()));
}
@Override
- public Uni save(I id, S state) {
+ public Uni save(I id, S state) {
store.put(id, state);
- return Uni.createFrom().item(entityCreator.apply(id, state));
+ return Uni.createFrom().item(entityCreator.apply(id, state)).map(Entity::id);
}
@Override
diff --git a/pta-common/src/main/java/ch/obya/pta/common/infrastructure/web/CommonAttributeConverterProvider.java b/pta-common/src/main/java/ch/obya/pta/common/infrastructure/web/CommonAttributeConverterProvider.java
new file mode 100644
index 0000000..1b7b23f
--- /dev/null
+++ b/pta-common/src/main/java/ch/obya/pta/common/infrastructure/web/CommonAttributeConverterProvider.java
@@ -0,0 +1,51 @@
+package ch.obya.pta.common.infrastructure.web;
+
+import ch.obya.pta.common.domain.vo.Name;
+import ch.obya.pta.common.domain.vo.Quota;
+import jakarta.ws.rs.ext.ParamConverter;
+import jakarta.ws.rs.ext.ParamConverterProvider;
+import jakarta.ws.rs.ext.Provider;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.Map;
+
+@Provider
+public class CommonAttributeConverterProvider implements ParamConverterProvider {
+
+ private final Map converters = Map.of(
+ Name.class, new NameConverter(),
+ Quota.class, new QuotaConverter()
+ );
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public ParamConverter getConverter(Class aClass, Type type, Annotation[] annotations) {
+ return converters.getOrDefault(aClass, null);
+ }
+
+ private static class NameConverter implements ParamConverter {
+ @Override
+ public Name fromString(String value) {
+ return new Name(value);
+ }
+
+ @Override
+ public String toString(Name value) {
+ return value.content();
+ }
+ }
+
+ private static class QuotaConverter implements ParamConverter {
+ @Override
+ public Quota fromString(String value) {
+ var fields = value.split(",");
+ return new Quota(Integer.valueOf(fields[0]), Integer.valueOf(fields[1]));
+ }
+
+ @Override
+ public String toString(Quota value) {
+ return "%d,%d".formatted(value.min(), value.max());
+ }
+ }
+}
diff --git a/pta-common/src/main/java/ch/obya/pta/common/util/search/AttributeFilter.java b/pta-common/src/main/java/ch/obya/pta/common/util/search/AttributeFilter.java
new file mode 100644
index 0000000..540dd7f
--- /dev/null
+++ b/pta-common/src/main/java/ch/obya/pta/common/util/search/AttributeFilter.java
@@ -0,0 +1,89 @@
+package ch.obya.pta.common.util.search;
+
+import jakarta.annotation.Nullable;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public record AttributeFilter(Operator operator, Object value) {
+
+ public interface Parser {
+ Object parse(String value);
+ }
+
+ public static List from(List filters, Parser parser) {
+ return filters.stream().map(filter -> from(filter, parser)).toList();
+ }
+
+ public static AttributeFilter from(String filter, Parser parser) {
+ Pattern pattern = Pattern.compile("(eq:|ne:|lt:|gt:|le:|ge:|like:)*(.+)");
+ Matcher matcher = pattern.matcher(filter);
+ if (matcher.find()) {
+ return matcher.groupCount() == 1 ?
+ new AttributeFilter(Operator.EQUALS, parser.parse(matcher.group(1))) :
+ new AttributeFilter(Operator.from(matcher.group(1)), parser.parse(matcher.group(2)));
+ }
+ throw new IllegalArgumentException("Invalid filter: " + filter);
+ }
+
+ public static AttributeFilter equal(Object value) {
+ return new AttributeFilter(Operator.EQUALS, value);
+ }
+
+ public static AttributeFilter notEqual(Object value) {
+ return new AttributeFilter(Operator.NOT_EQUALS, value);
+ }
+
+ public static AttributeFilter lessThan(Object value) {
+ return new AttributeFilter(Operator.LESS_THAN, value);
+ }
+
+ public static AttributeFilter lessThanOrEqual(Object value) {
+ return new AttributeFilter(Operator.LESS_THAN_EQUALS, value);
+ }
+
+ public static AttributeFilter greaterThan(Object value) {
+ return new AttributeFilter(Operator.GREATER_THAN, value);
+ }
+
+ public static AttributeFilter greaterThanOrEqual(Object value) {
+ return new AttributeFilter(Operator.GREATER_THAN_EQUALS, value);
+ }
+
+ public static AttributeFilter like(Object value) {
+ return new AttributeFilter(Operator.LIKE, value);
+ }
+
+ public enum Operator {
+ EQUALS("eq:"),
+ NOT_EQUALS("ne:"),
+ LESS_THAN("lt:"),
+ LESS_THAN_EQUALS("le:"),
+ GREATER_THAN("gt:"),
+ GREATER_THAN_EQUALS("ge:"),
+ LIKE("like:");
+
+ private final String symbol;
+
+ Operator(String symbol) {
+ this.symbol = symbol;
+ }
+
+ public String symbol() {
+ return symbol;
+ }
+
+ public static Operator from(@Nullable String symbol) {
+ if (symbol == null) {
+ return EQUALS;
+ }
+ for (Operator operator : Operator.values()) {
+ if (operator.symbol().equals(symbol)) {
+ return operator;
+ }
+ }
+ throw new IllegalArgumentException("Unknown filtering operation: " + symbol);
+ }
+ }
+}
diff --git a/pta-common/src/main/java/ch/obya/pta/common/util/search/FindCriteria.java b/pta-common/src/main/java/ch/obya/pta/common/util/search/FindCriteria.java
deleted file mode 100644
index c84c9d9..0000000
--- a/pta-common/src/main/java/ch/obya/pta/common/util/search/FindCriteria.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package ch.obya.pta.common.util.search;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public record FindCriteria(String key, String operation, Object value) {
-
- public static Collection empty() {
- return Collections.emptyList();
- }
-
- public static Collection from(String filter) {
- List result = new ArrayList<>();
- Pattern pattern = Pattern.compile("(\\w+?)(:|<|>)(\\w+?),");
- Matcher matcher = pattern.matcher(filter + ",");
- while (matcher.find()) {
- result.add(new FindCriteria(matcher.group(1), matcher.group(2), matcher.group(3)));
- }
- return result;
- }
-}
diff --git a/pta-common/src/test/java/ch/obya/pta/common/util/AttributeFilterTest.java b/pta-common/src/test/java/ch/obya/pta/common/util/AttributeFilterTest.java
new file mode 100644
index 0000000..a7e728e
--- /dev/null
+++ b/pta-common/src/test/java/ch/obya/pta/common/util/AttributeFilterTest.java
@@ -0,0 +1,15 @@
+package ch.obya.pta.common.util;
+
+import ch.obya.pta.common.util.search.AttributeFilter;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AttributeFilterTest {
+
+ @Test
+ void should_build_attribute_filter() {
+ assertThat(AttributeFilter.from("eq:albert", s -> s)).isEqualTo(AttributeFilter.equal("albert"));
+ assertThat(AttributeFilter.from("eq:1958-12-12", s -> s)).isEqualTo(AttributeFilter.equal("1958-12-12"));
+ }
+}
diff --git a/pta-common/src/test/java/ch/obya/pta/common/util/FindCriteriaTest.java b/pta-common/src/test/java/ch/obya/pta/common/util/FindCriteriaTest.java
deleted file mode 100644
index 2d82d7c..0000000
--- a/pta-common/src/test/java/ch/obya/pta/common/util/FindCriteriaTest.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package ch.obya.pta.common.util;
-
-import org.junit.jupiter.api.Test;
-
-import ch.obya.pta.common.util.search.FindCriteria;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-class FindCriteriaTest {
-
- @Test
- void should_build_criteria_from_filter() {
- assertThat(FindCriteria.from("firstName:albert"))
- .containsExactly(new FindCriteria("firstName", ":", "albert"));
-
- assertThat(FindCriteria.from("firstName:albert,min>0,max<10"))
- .containsExactlyInAnyOrder(
- new FindCriteria("firstName", ":", "albert"),
- new FindCriteria("min", ">", "0"),
- new FindCriteria("max", "<", "10"));
- }
-}
diff --git a/pta-customer/doc/openapi.json b/pta-customer/doc/openapi.json
index 8f3d704..9cff346 100644
--- a/pta-customer/doc/openapi.json
+++ b/pta-customer/doc/openapi.json
@@ -27,10 +27,58 @@
"get" : {
"tags" : [ "Customer Resource" ],
"parameters" : [ {
- "name" : "filter",
+ "name" : "birth",
"in" : "query",
"schema" : {
- "type" : "string"
+ "type" : "array",
+ "items" : {
+ "type" : "string"
+ }
+ }
+ }, {
+ "name" : "email",
+ "in" : "query",
+ "schema" : {
+ "type" : "array",
+ "items" : {
+ "type" : "string"
+ }
+ }
+ }, {
+ "name" : "firstname",
+ "in" : "query",
+ "schema" : {
+ "type" : "array",
+ "items" : {
+ "type" : "string"
+ }
+ }
+ }, {
+ "name" : "gender",
+ "in" : "query",
+ "schema" : {
+ "type" : "array",
+ "items" : {
+ "type" : "string"
+ }
+ }
+ }, {
+ "name" : "lastname",
+ "in" : "query",
+ "schema" : {
+ "type" : "array",
+ "items" : {
+ "type" : "string"
+ }
+ }
+ }, {
+ "name" : "phone",
+ "in" : "query",
+ "schema" : {
+ "type" : "array",
+ "items" : {
+ "type" : "string"
+ }
}
} ],
"responses" : {
@@ -161,10 +209,10 @@
"$ref" : "#/components/schemas/PhysicalAddress"
}
}, {
- "name" : "birthdate",
+ "name" : "birth",
"in" : "query",
"schema" : {
- "$ref" : "#/components/schemas/BirthDate"
+ "$ref" : "#/components/schemas/Birth"
}
}, {
"name" : "delivery",
@@ -217,7 +265,7 @@
}
},
"/customers/{id}/connection" : {
- "put" : {
+ "patch" : {
"tags" : [ "Customer Resource" ],
"parameters" : [ {
"name" : "id",
@@ -247,7 +295,7 @@
}
},
"/customers/{id}/definition" : {
- "put" : {
+ "patch" : {
"tags" : [ "Customer Resource" ],
"parameters" : [ {
"name" : "id",
@@ -257,10 +305,10 @@
"$ref" : "#/components/schemas/CustomerId"
}
}, {
- "name" : "birthdate",
+ "name" : "birth",
"in" : "query",
"schema" : {
- "$ref" : "#/components/schemas/BirthDate"
+ "$ref" : "#/components/schemas/Birth"
}
}, {
"name" : "gender",
@@ -277,7 +325,7 @@
}
},
"/customers/{id}/location" : {
- "put" : {
+ "patch" : {
"tags" : [ "Customer Resource" ],
"parameters" : [ {
"name" : "id",
@@ -307,7 +355,7 @@
}
},
"/customers/{id}/naming" : {
- "put" : {
+ "patch" : {
"tags" : [ "Customer Resource" ],
"parameters" : [ {
"name" : "id",
@@ -381,7 +429,7 @@
}
}
},
- "BirthDate" : {
+ "Birth" : {
"type" : "object",
"properties" : {
"date" : {
@@ -404,22 +452,22 @@
"lastName" : {
"$ref" : "#/components/schemas/Name"
},
- "birthDate" : {
- "$ref" : "#/components/schemas/BirthDate"
+ "birth" : {
+ "$ref" : "#/components/schemas/Birth"
},
"gender" : {
"$ref" : "#/components/schemas/Gender"
},
- "deliveryAddress" : {
+ "delivery" : {
"$ref" : "#/components/schemas/PhysicalAddress"
},
- "billingAddress" : {
+ "billing" : {
"$ref" : "#/components/schemas/PhysicalAddress"
},
- "emailAddress" : {
+ "email" : {
"$ref" : "#/components/schemas/EmailAddress"
},
- "phoneNumber" : {
+ "phone" : {
"$ref" : "#/components/schemas/PhoneNumber"
},
"notes" : {
diff --git a/pta-customer/doc/openapi.yaml b/pta-customer/doc/openapi.yaml
index b10e1a6..a9fe0b1 100644
--- a/pta-customer/doc/openapi.yaml
+++ b/pta-customer/doc/openapi.yaml
@@ -23,10 +23,42 @@ paths:
tags:
- Customer Resource
parameters:
- - name: filter
+ - name: birth
in: query
schema:
- type: string
+ type: array
+ items:
+ type: string
+ - name: email
+ in: query
+ schema:
+ type: array
+ items:
+ type: string
+ - name: firstname
+ in: query
+ schema:
+ type: array
+ items:
+ type: string
+ - name: gender
+ in: query
+ schema:
+ type: array
+ items:
+ type: string
+ - name: lastname
+ in: query
+ schema:
+ type: array
+ items:
+ type: string
+ - name: phone
+ in: query
+ schema:
+ type: array
+ items:
+ type: string
responses:
"200":
description: OK
@@ -114,10 +146,10 @@ paths:
in: query
schema:
$ref: "#/components/schemas/PhysicalAddress"
- - name: birthdate
+ - name: birth
in: query
schema:
- $ref: "#/components/schemas/BirthDate"
+ $ref: "#/components/schemas/Birth"
- name: delivery
in: query
schema:
@@ -150,7 +182,7 @@ paths:
"204":
description: No Content
/customers/{id}/connection:
- put:
+ patch:
tags:
- Customer Resource
parameters:
@@ -171,7 +203,7 @@ paths:
"204":
description: No Content
/customers/{id}/definition:
- put:
+ patch:
tags:
- Customer Resource
parameters:
@@ -180,10 +212,10 @@ paths:
required: true
schema:
$ref: "#/components/schemas/CustomerId"
- - name: birthdate
+ - name: birth
in: query
schema:
- $ref: "#/components/schemas/BirthDate"
+ $ref: "#/components/schemas/Birth"
- name: gender
in: query
schema:
@@ -192,7 +224,7 @@ paths:
"204":
description: No Content
/customers/{id}/location:
- put:
+ patch:
tags:
- Customer Resource
parameters:
@@ -213,7 +245,7 @@ paths:
"204":
description: No Content
/customers/{id}/naming:
- put:
+ patch:
tags:
- Customer Resource
parameters:
@@ -263,7 +295,7 @@ components:
properties:
id:
$ref: "#/components/schemas/UUID"
- BirthDate:
+ Birth:
type: object
properties:
date:
@@ -279,17 +311,17 @@ components:
$ref: "#/components/schemas/Name"
lastName:
$ref: "#/components/schemas/Name"
- birthDate:
- $ref: "#/components/schemas/BirthDate"
+ birth:
+ $ref: "#/components/schemas/Birth"
gender:
$ref: "#/components/schemas/Gender"
- deliveryAddress:
+ delivery:
$ref: "#/components/schemas/PhysicalAddress"
- billingAddress:
+ billing:
$ref: "#/components/schemas/PhysicalAddress"
- emailAddress:
+ email:
$ref: "#/components/schemas/EmailAddress"
- phoneNumber:
+ phone:
$ref: "#/components/schemas/PhoneNumber"
notes:
type: string
diff --git a/pta-customer/pom.xml b/pta-customer/pom.xml
index 2efa032..718e370 100644
--- a/pta-customer/pom.xml
+++ b/pta-customer/pom.xml
@@ -8,13 +8,13 @@
../pta-parent/pom.xml
pta-customer
- 0.2.1
+ 0.3.0
jar
${project.groupId}
pta-common
- 0.3.0
+ 0.4.0
com.googlecode.libphonenumber
@@ -22,12 +22,16 @@
8.13.36
- io.quarkiverse.quinoa
- quarkus-quinoa
+ io.quarkus
+ quarkus-arc
io.quarkus
- quarkus-arc
+ quarkus-config-yaml
+
+
+ io.quarkiverse.quinoa
+ quarkus-quinoa
io.quarkus
@@ -53,10 +57,35 @@
com.tietoevry.quarkus
quarkus-resteasy-problem
+
+ io.quarkus
+ quarkus-hibernate-reactive-panache
+
+
+ io.quarkus
+ quarkus-hibernate-validator
+
+
+ io.quarkus
+ quarkus-reactive-pg-client
+
+
+ io.quarkus
+ quarkus-jdbc-postgresql
+
+
+ io.quarkus
+ quarkus-flyway
+
org.zalando
problem
+
+ io.quarkus
+ quarkus-test-hibernate-reactive-panache
+ test
+
io.quarkus
quarkus-junit5
diff --git a/pta-customer/src/main/java/ch/obya/pta/customer/application/CustomerService.java b/pta-customer/src/main/java/ch/obya/pta/customer/application/CustomerService.java
index e05d86a..afad5e9 100644
--- a/pta-customer/src/main/java/ch/obya/pta/customer/application/CustomerService.java
+++ b/pta-customer/src/main/java/ch/obya/pta/customer/application/CustomerService.java
@@ -1,23 +1,27 @@
package ch.obya.pta.customer.application;
import ch.obya.pta.common.application.EventPublisher;
-import ch.obya.pta.common.domain.util.EntityFinder;
import ch.obya.pta.common.domain.util.CommonProblem;
-import ch.obya.pta.common.util.search.FindCriteria;
+import ch.obya.pta.common.domain.util.EntityFinder;
import ch.obya.pta.customer.domain.aggregate.Customer;
import ch.obya.pta.customer.domain.event.CustomerRemoved;
import ch.obya.pta.customer.domain.repository.CustomerRepository;
import ch.obya.pta.customer.domain.util.CustomerProblem;
import ch.obya.pta.customer.domain.vo.*;
+import io.quarkus.hibernate.reactive.panache.common.WithTransaction;
import io.smallrye.mutiny.Uni;
+import jakarta.annotation.Nullable;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
-import jakarta.transaction.Transactional;
-import java.time.Duration;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
+import static ch.obya.pta.common.util.search.AttributeFilter.equal;
+import static ch.obya.pta.common.util.search.AttributeFilter.notEqual;
+
@ApplicationScoped
public class CustomerService {
@@ -26,7 +30,7 @@ public class CustomerService {
@Inject
EventPublisher eventPublisher;
- @Transactional
+ @WithTransaction
public Uni create(Person person,
PhysicalAddress deliveryAddress,
PhysicalAddress billingAddress,
@@ -36,20 +40,21 @@ public Uni create(Person person,
return checkUniqueness(null, person, emailAddress).replaceWith(
Uni.createFrom().item(
- new Customer(
- person,
- deliveryAddress,
- billingAddress,
- emailAddress,
- phoneNumber,
- notes
- ))
- .flatMap(c -> repository.save(c))
- .flatMap(c -> eventPublisher.publish(c.domainEvents()).replaceWith(c))
- .map(Customer::id));
+ new Customer(
+ person,
+ deliveryAddress,
+ billingAddress,
+ emailAddress,
+ phoneNumber,
+ notes
+ ))
+ .flatMap(c -> repository.save(c).replaceWith(c))
+ .flatMap(c -> eventPublisher.publish(c.domainEvents()).replaceWith(c))
+ .map(Customer::id)
+ );
}
- @Transactional
+ @WithTransaction
public Uni modify(CustomerId customerId, Consumer patches) {
return findOne(customerId)
.map(c -> {
@@ -62,31 +67,38 @@ public Uni modify(CustomerId customerId, Consumer patch
var state = c.state();
return checkUniqueness(customerId, state.person(), state.emailAddress()).replaceWith(c);
})
- .flatMap(c -> repository.save(c))
+ .flatMap(c -> repository.save(c).replaceWith(c))
.flatMap(c -> eventPublisher.publish(c.domainEvents()));
}
- private Uni checkUniqueness(CustomerId exclude, Person person, EmailAddress emailAddress) {
- return repository.findByCriteria(FindCriteria.from("""
- firstName:%s,
- lastName:%s,
- birthDate:%s
- emailAddress:%s
- """.formatted(person.firstName(), person.lastName(), person.birthDate(), emailAddress.address())))
- .select().where(c -> !c.id().equals(exclude))
- .onItem().failWith(CustomerProblem.AlreadyExisting.toException(
- person.firstName(), person.lastName(), person.birthDate(), emailAddress))
- .ifNoItem().after(Duration.ofMillis(100))
- .recoverWithCompletion().toUni().replaceWithVoid();
+ private Uni checkUniqueness(@Nullable CustomerId exclude, Person person, EmailAddress emailAddress) {
+ return repository.findByCriteria(exclude != null ?
+ Map.of(
+ "id", List.of(notEqual(exclude)),
+ "firstName", List.of(equal(person.firstName())),
+ "lastName", List.of(equal(person.lastName())),
+ "birth", List.of(equal(person.birth())),
+ "email", List.of(equal(emailAddress))
+ ): Map.of(
+ "firstName", List.of(equal(person.firstName())),
+ "lastName", List.of(equal(person.lastName())),
+ "birth", List.of(equal(person.birth())),
+ "email", List.of(equal(emailAddress))
+ ))
+ .flatMap(l -> l.isEmpty() ?
+ Uni.createFrom().voidItem() :
+ Uni.createFrom().failure(CustomerProblem.AlreadyExisting
+ .toException(person.firstName(), person.lastName(), person.birth(), emailAddress).get()));
}
- @Transactional
+ @WithTransaction
public Uni remove(CustomerId customerId, Boolean force) {
return findOne(customerId)
.flatMap(c -> (force ? repository.remove(customerId) : repository.save(c.close())).replaceWith(c))
.flatMap(c -> eventPublisher.publish(force ? Set.of(new CustomerRemoved(customerId)) : c.domainEvents()));
}
+ @WithTransaction
public Uni findOne(CustomerId id) {
return EntityFinder.find(Customer.class, id, repository::findOne, CommonProblem.EntityNotFound);
}
diff --git a/pta-customer/src/main/java/ch/obya/pta/customer/domain/aggregate/Customer.java b/pta-customer/src/main/java/ch/obya/pta/customer/domain/aggregate/Customer.java
index e07a588..9d1690b 100644
--- a/pta-customer/src/main/java/ch/obya/pta/customer/domain/aggregate/Customer.java
+++ b/pta-customer/src/main/java/ch/obya/pta/customer/domain/aggregate/Customer.java
@@ -35,8 +35,8 @@ public record State(
public State {
ifNullThrow(person, CommonProblem.AttributeNotNull.toException("Customer.person"));
- ifNullThrow(person, CommonProblem.AttributeNotNull.toException("Customer.emailAddress"));
- ifNullThrow(person, CommonProblem.AttributeNotNull.toException("Customer.phoneNumber"));
+ ifNullThrow(person, CommonProblem.AttributeNotNull.toException("Customer.email"));
+ ifNullThrow(person, CommonProblem.AttributeNotNull.toException("Customer.phone"));
}
}
@@ -122,8 +122,8 @@ public Modifier rename(Name firstName, Name lastName) {
return this;
}
- public Modifier redefine(Person.BirthDate birthDate, Person.Gender gender) {
- if (birthDate != null) personBuilder.birthDate(birthDate);
+ public Modifier redefine(Person.Birth birth, Person.Gender gender) {
+ if (birth != null) personBuilder.birth(birth);
if (gender != null) personBuilder.gender(gender);
return this;
}
diff --git a/pta-customer/src/main/java/ch/obya/pta/customer/domain/util/Samples.java b/pta-customer/src/main/java/ch/obya/pta/customer/domain/util/Samples.java
index a871305..9f15cf4 100644
--- a/pta-customer/src/main/java/ch/obya/pta/customer/domain/util/Samples.java
+++ b/pta-customer/src/main/java/ch/obya/pta/customer/domain/util/Samples.java
@@ -15,18 +15,32 @@
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Samples {
- public static Customer customer() {
+ public static Customer johnDoe() {
return new Customer(
new Person("Mr",
new Name("john"),
new Name("doe"),
- new Person.BirthDate(LocalDate.of(1966, 4, 5)),
+ new Person.Birth(LocalDate.of(1966, 4, 5)),
Person.Gender.MALE),
new PhysicalAddress("test-delivery", "zip", "city", "region", "ch"),
new PhysicalAddress("test-billing", "zip", "city", "region", "ch"),
new EmailAddress("john@doe.ch"),
- new PhoneNumber("0041781234567"),
- "notes");
+ new PhoneNumber("+41781234567"),
+ "no notes");
+ }
+
+ public static Customer sherlockHolmes() {
+ return new Customer(
+ new Person("Mr",
+ new Name("sherlock"),
+ new Name("holmes"),
+ new Person.Birth(LocalDate.of(1966, 4, 5)),
+ Person.Gender.MALE),
+ new PhysicalAddress("221B Baker St", "NW1 6XE", "london", "Marylebone", "uk"),
+ new PhysicalAddress("221B Baker St", "NW1 6XE", "london", "Marylebone", "uk"),
+ new EmailAddress("sherlock@holmes.uk"),
+ new PhoneNumber("+4402072243688"),
+ "no comment");
}
private static Subscription subscription() {
@@ -39,7 +53,7 @@ private static Subscription subscription() {
ArticleId.create());
}
- public static final Supplier oneCustomer = Samples::customer;
+ public static final Supplier oneCustomer = Samples::johnDoe;
public static final Supplier oneCustomerWithOneYearSubscription = () -> {
var c = oneCustomer.get();
diff --git a/pta-customer/src/main/java/ch/obya/pta/customer/domain/vo/Person.java b/pta-customer/src/main/java/ch/obya/pta/customer/domain/vo/Person.java
index ec93f74..7bfa1dd 100644
--- a/pta-customer/src/main/java/ch/obya/pta/customer/domain/vo/Person.java
+++ b/pta-customer/src/main/java/ch/obya/pta/customer/domain/vo/Person.java
@@ -5,23 +5,25 @@
import ch.obya.pta.customer.domain.util.CustomerProblem;
import lombok.Builder;
+import java.text.DateFormat;
import java.time.LocalDate;
import static ch.obya.pta.common.domain.util.CommonProblem.*;
+import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;
@Builder(builderClassName = "Builder", toBuilder = true)
public record Person(
String salutation,
Name firstName,
Name lastName,
- BirthDate birthDate,
+ Birth birth,
Gender gender
) {
public Person {
ifNullThrow(firstName, CommonProblem.AttributeNotNull.toException("Person.firstName"));
ifNullThrow(lastName, CommonProblem.AttributeNotNull.toException("Person.lastName"));
- ifNullThrow(birthDate, CommonProblem.AttributeNotNull.toException("Person.birthDate"));
+ ifNullThrow(birth, CommonProblem.AttributeNotNull.toException("Person.birth"));
ifNullThrow(gender, CommonProblem.AttributeNotNull.toException("Person.gender"));
}
@@ -29,11 +31,19 @@ public String fullName() {
return "%s %s".formatted(firstName.content(), lastName.content());
}
- public record BirthDate(LocalDate date) {
- public BirthDate {
- ifNullThrow(date, CommonProblem.AttributeNotNull.toException("BirthDate.date"));
+ public record Birth(LocalDate date) {
+ public Birth {
+ ifNullThrow(date, CommonProblem.AttributeNotNull.toException("birth.date"));
ifThrow(() -> date.isAfter(LocalDate.now()), CustomerProblem.BirthDateInvalid.toException(date));
}
+
+ public static Birth fromISO(String s) {
+ return new Birth(LocalDate.parse(s, ISO_LOCAL_DATE));
+ }
+
+ public String toISO() {
+ return ISO_LOCAL_DATE.format(date);
+ }
}
public enum Gender { MALE, FEMALE }
diff --git a/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/data/DefaultCustomerRepository.java b/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/data/DefaultCustomerRepository.java
deleted file mode 100644
index 8ee10df..0000000
--- a/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/data/DefaultCustomerRepository.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package ch.obya.pta.customer.infrastructure.data;
-
-import ch.obya.pta.common.util.search.FindCriteria;
-import ch.obya.pta.customer.domain.aggregate.Customer;
-import ch.obya.pta.customer.domain.repository.CustomerRepository;
-import ch.obya.pta.customer.domain.vo.CustomerId;
-import io.smallrye.mutiny.Multi;
-import io.smallrye.mutiny.Uni;
-import jakarta.enterprise.context.ApplicationScoped;
-
-import java.util.Collection;
-
-@ApplicationScoped
-public class DefaultCustomerRepository implements CustomerRepository {
- @Override
- public Uni findOne(CustomerId id) {
- return null;
- }
-
- @Override
- public Multi findByCriteria(Collection criteria) {
- return null;
- }
-
- @Override
- public Uni save(CustomerId id, Customer.State state) {
- return null;
- }
-
- @Override
- public Uni remove(CustomerId id) {
- return null;
- }
-}
diff --git a/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/data/PanacheCustomerEntity.java b/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/data/PanacheCustomerEntity.java
new file mode 100644
index 0000000..1632f97
--- /dev/null
+++ b/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/data/PanacheCustomerEntity.java
@@ -0,0 +1,138 @@
+package ch.obya.pta.customer.infrastructure.data;
+
+import ch.obya.pta.common.domain.vo.Name;
+import ch.obya.pta.common.util.search.AttributeFilter;
+import ch.obya.pta.customer.domain.aggregate.Customer;
+import ch.obya.pta.customer.domain.vo.*;
+import io.quarkus.hibernate.reactive.panache.PanacheEntity;
+import io.quarkus.hibernate.reactive.panache.PanacheQuery;
+import io.quarkus.panache.common.Parameters;
+import jakarta.persistence.Entity;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+@Entity(name = "customer")
+class PanacheCustomerEntity extends PanacheEntity {
+ UUID logicalId;
+ String salutation;
+ String firstName;
+ String lastName;
+ LocalDate birth;
+ Person.Gender gender;
+ String streetNo;
+ String zip;
+ String city;
+ String region;
+ String country;
+ String email;
+ String phone;
+
+ static PanacheQuery findByLogicalId(CustomerId id) {
+ return find("logicalId", id.id());
+ }
+
+ static PanacheQuery findByCriteria(Map> criteria) {
+ return find(toQuery(criteria), toParameters(criteria));
+ }
+
+ private static String toQuery(Map> criteria) {
+ return criteria.entrySet().stream()
+ .map(e -> toComparison(e.getKey(), e.getValue()))
+ .reduce("%s and %s"::formatted)
+ .orElse("");
+ }
+
+ private static String toComparison(String name, List value) {
+ return value.stream()
+ .map(f -> "%s %s :%s".formatted(toDataField(name), toOperator(f.operator()), toDataField(name)))
+ .reduce("%s and %s"::formatted)
+ .orElse("");
+ }
+
+ private static String toOperator(AttributeFilter.Operator operator) {
+ return switch (operator) {
+ case EQUALS -> "=";
+ case NOT_EQUALS -> "!=";
+ case GREATER_THAN -> ">";
+ case GREATER_THAN_EQUALS -> ">=";
+ case LESS_THAN -> "<";
+ case LESS_THAN_EQUALS -> "<=";
+ case LIKE -> "like";
+ };
+ }
+
+ private static Parameters toParameters(Map> criteria) {
+ var parameters = new Parameters();
+ for(var filters: criteria.entrySet()) {
+ for (var filter: filters.getValue()) {
+ parameters = parameters.and(toDataField(filters.getKey()), toParameter(filter.value()));
+ }
+ }
+ return parameters;
+ }
+
+ private static Object toParameter(Object value) {
+ return switch (value) {
+ case CustomerId id -> id.id();
+ case Name name -> name.content();
+ case PhoneNumber phoneNumber -> phoneNumber.number();
+ case EmailAddress emailAddress -> emailAddress.address();
+ case Person.Birth birth -> birth.date();
+ default -> value;
+ };
+ }
+
+ private static String toDataField(String name) {
+ return switch (name) {
+ case "id" -> "logicalId";
+ case "lastName" -> "lastName";
+ case "firstName" -> "firstName";
+ case "birth" -> "birth";
+ case "gender" -> "gender";
+ case "streetNo" -> "streetNo";
+ case "zip" -> "zip";
+ case "city" -> "city";
+ case "region" -> "region";
+ case "country" -> "country";
+ case "email" -> "email";
+ case "phone" -> "phone";
+ default -> throw new IllegalArgumentException("Unknown attribute: " + name);
+ };
+ }
+
+ Customer toDomain() {
+ return new Customer(
+ new CustomerId(logicalId),
+ new Person(salutation,
+ new Name(firstName),
+ new Name(lastName),
+ new Person.Birth(birth),
+ gender),
+ new PhysicalAddress(streetNo, zip, city, region, country),
+ null,
+ new EmailAddress("john@doe.ch"),
+ new PhoneNumber("0041781234567"),
+ "notes"
+ );
+ }
+
+ PanacheCustomerEntity set(CustomerId id, Customer.State state) {
+ logicalId = id.id();
+ salutation = state.person().salutation();
+ firstName = state.person().firstName().content();
+ lastName = state.person().lastName().content();
+ birth = state.person().birth().date();
+ gender = state.person().gender();
+ streetNo = state.deliveryAddress().streetNo();
+ zip = state.deliveryAddress().zip();
+ city = state.deliveryAddress().city();
+ region = state.deliveryAddress().region();
+ country = state.deliveryAddress().country();
+ email = state.emailAddress().address();
+ phone = state.phoneNumber().number();
+ return this;
+ }
+}
diff --git a/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/data/PanacheCustomerRepositoryAdapter.java b/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/data/PanacheCustomerRepositoryAdapter.java
new file mode 100644
index 0000000..46954fa
--- /dev/null
+++ b/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/data/PanacheCustomerRepositoryAdapter.java
@@ -0,0 +1,55 @@
+package ch.obya.pta.customer.infrastructure.data;
+
+import ch.obya.pta.common.util.search.AttributeFilter;
+import ch.obya.pta.customer.domain.aggregate.Customer;
+import ch.obya.pta.customer.domain.repository.CustomerRepository;
+import ch.obya.pta.customer.domain.vo.CustomerId;
+import io.quarkus.hibernate.reactive.panache.common.WithTransaction;
+import io.smallrye.mutiny.Uni;
+import jakarta.enterprise.context.ApplicationScoped;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+
+@WithTransaction
+@ApplicationScoped
+public class PanacheCustomerRepositoryAdapter implements CustomerRepository {
+
+ @Override
+ public Uni findOne(CustomerId id) {
+ return PanacheCustomerEntity.findByLogicalId(id)
+ .firstResult()
+ .map(PanacheCustomerEntity::toDomain);
+ }
+
+ @Override
+ public Uni> findByCriteria(Map> criteria) {
+ return PanacheCustomerEntity.findByCriteria(criteria)
+ .list()
+ .map(l -> l.stream().map(PanacheCustomerEntity::toDomain).toList());
+ }
+
+ @Override
+ public Uni> findAll() {
+ return PanacheCustomerEntity.findAll()
+ .list()
+ .map(l -> l.stream().map(PanacheCustomerEntity::toDomain).toList());
+ }
+
+ @Override
+ public Uni save(CustomerId id, Customer.State state) {
+ return PanacheCustomerEntity.findByLogicalId(id)
+ .firstResult()
+ .ifNoItem().after(Duration.ofMillis(100)).recoverWithUni(() -> Uni.createFrom().item(new PanacheCustomerEntity()))
+ .replaceIfNullWith(new PanacheCustomerEntity())
+ .map(e -> e.set(id, state))
+ .flatMap(e -> e.persist())
+ .replaceWith(id);
+ }
+
+ @Override
+ public Uni remove(CustomerId id) {
+ return PanacheCustomerEntity.delete("logicalId", id.id()).replaceWithVoid();
+ }
+}
diff --git a/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/web/CustomerAttributeConverterProvider.java b/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/web/CustomerAttributeConverterProvider.java
index ce7d8d2..42e3737 100644
--- a/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/web/CustomerAttributeConverterProvider.java
+++ b/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/web/CustomerAttributeConverterProvider.java
@@ -1,7 +1,5 @@
package ch.obya.pta.customer.infrastructure.web;
-import ch.obya.pta.common.domain.vo.Name;
-import ch.obya.pta.common.domain.vo.Quota;
import ch.obya.pta.customer.domain.vo.*;
import jakarta.ws.rs.ext.ParamConverter;
import jakarta.ws.rs.ext.ParamConverterProvider;
@@ -9,23 +7,18 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
-import java.time.LocalDate;
import java.util.Map;
import java.util.UUID;
-import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;
-
@Provider
public class CustomerAttributeConverterProvider implements ParamConverterProvider {
private final Map converters = Map.of(
CustomerId.class, new CustomerIdConverter(),
- Name.class, new NameConverter(),
- Person.BirthDate.class, new BirthDateConverter(),
+ Person.Birth.class, new BirthDateConverter(),
EmailAddress.class, new EmailAddressConverter(),
PhoneNumber.class, new PhoneNumberConverter(),
- PhysicalAddress.class, new PhysicalAddressConverter(),
- Quota.class, new QuotaConverter()
+ PhysicalAddress.class, new PhysicalAddressConverter()
);
@SuppressWarnings("unchecked")
@@ -34,15 +27,15 @@ public ParamConverter getConverter(Class aClass, Type type, Annotation
return converters.getOrDefault(aClass, null);
}
- private static class BirthDateConverter implements ParamConverter {
+ private static class BirthDateConverter implements ParamConverter {
@Override
- public Person.BirthDate fromString(String value) {
- return new Person.BirthDate(LocalDate.parse(value, ISO_LOCAL_DATE));
+ public Person.Birth fromString(String value) {
+ return Person.Birth.fromISO(value);
}
@Override
- public String toString(Person.BirthDate value) {
- return ISO_LOCAL_DATE.format(value.date());
+ public String toString(Person.Birth value) {
+ return value.toISO();
}
}
@@ -70,18 +63,6 @@ public String toString(EmailAddress value) {
}
}
- private static class NameConverter implements ParamConverter {
- @Override
- public Name fromString(String value) {
- return new Name(value);
- }
-
- @Override
- public String toString(Name value) {
- return value.content();
- }
- }
-
private static class PhoneNumberConverter implements ParamConverter {
@Override
public PhoneNumber fromString(String value) {
@@ -106,17 +87,4 @@ public String toString(PhysicalAddress value) {
return "%s,%s,%s,%s,%s".formatted(value.streetNo(), value.zip(), value.city(), value.region(), value.country());
}
}
-
- private static class QuotaConverter implements ParamConverter {
- @Override
- public Quota fromString(String value) {
- var fields = value.split(",");
- return new Quota(Integer.valueOf(fields[0]), Integer.valueOf(fields[1]));
- }
-
- @Override
- public String toString(Quota value) {
- return "%d,%d".formatted(value.min(), value.max());
- }
- }
}
diff --git a/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/web/CustomerDto.java b/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/web/CustomerDto.java
index ee909f9..6604537 100644
--- a/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/web/CustomerDto.java
+++ b/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/web/CustomerDto.java
@@ -7,31 +7,31 @@
@Builder(builderClassName = "Builder", toBuilder = true)
public record CustomerDto(
- CustomerId id,
- String salutation,
- Name firstName,
- Name lastName,
- Person.BirthDate birthDate,
- Person.Gender gender,
- PhysicalAddress deliveryAddress,
- PhysicalAddress billingAddress,
- EmailAddress emailAddress,
- PhoneNumber phoneNumber,
- String notes) {
+ CustomerId id,
+ String salutation,
+ Name firstName,
+ Name lastName,
+ Person.Birth birth,
+ Person.Gender gender,
+ PhysicalAddress delivery,
+ PhysicalAddress billing,
+ EmailAddress email,
+ PhoneNumber phone,
+ String notes) {
public static CustomerDto from(Customer customer) {
var state = customer.state();
return CustomerDto.builder()
.id(customer.id())
.salutation(state.person().salutation())
- .firstName( state.person().firstName())
- .lastName( state.person().lastName())
- .birthDate( state.person().birthDate())
- .gender( state.person().gender())
- .deliveryAddress( state.deliveryAddress())
- .billingAddress( state.billingAddress())
- .emailAddress( state.emailAddress())
- .phoneNumber( state.phoneNumber())
- .notes( state.notes()).build();
+ .firstName(state.person().firstName())
+ .lastName(state.person().lastName())
+ .birth(state.person().birth())
+ .gender(state.person().gender())
+ .delivery(state.deliveryAddress())
+ .billing(state.billingAddress())
+ .email(state.emailAddress())
+ .phone(state.phoneNumber())
+ .notes(state.notes()).build();
}
}
diff --git a/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/web/CustomerResource.java b/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/web/CustomerResource.java
index c353a3e..4c188f4 100644
--- a/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/web/CustomerResource.java
+++ b/pta-customer/src/main/java/ch/obya/pta/customer/infrastructure/web/CustomerResource.java
@@ -1,7 +1,7 @@
package ch.obya.pta.customer.infrastructure.web;
import ch.obya.pta.common.domain.vo.Name;
-import ch.obya.pta.common.util.search.FindCriteria;
+import ch.obya.pta.common.util.search.AttributeFilter;
import ch.obya.pta.customer.application.CustomerService;
import ch.obya.pta.customer.domain.repository.CustomerRepository;
import ch.obya.pta.customer.domain.vo.*;
@@ -10,11 +10,15 @@
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import org.jboss.resteasy.reactive.ResponseStatus;
+import org.jboss.resteasy.reactive.RestPath;
import org.jboss.resteasy.reactive.RestQuery;
import org.zalando.problem.Problem;
import org.zalando.problem.Status;
import java.time.LocalDate;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
@Produces("application/problem+json")
@Path("/customers")
@@ -33,102 +37,125 @@ public Uni create(CustomerDto input) {
input.salutation(),
input.firstName(),
input.lastName(),
- input.birthDate(),
+ input.birth(),
input.gender()
),
- input.deliveryAddress(),
- input.billingAddress(),
- input.emailAddress(),
- input.phoneNumber(),
- input.notes());
+ input.delivery(),
+ input.billing(),
+ input.email(),
+ input.phone(),
+ input.notes())
+ .onFailure().transform(t -> Problem.valueOf(Status.BAD_REQUEST, t.getMessage()));
}
@ResponseStatus(204)
@PUT
@Path("/{id}")
- public Uni modify(CustomerId id, CustomerDto input) {
+ public Uni replace(@RestPath CustomerId id, CustomerDto input) {
return service.modify(id, customer -> customer
.rename(input.firstName(), input.lastName())
- .redefine(input.birthDate(), input.gender())
- .reconnect(input.emailAddress(), input.phoneNumber())
- .relocate(input.deliveryAddress(), input.billingAddress())
- .annotate(input.notes()));
+ .redefine(input.birth(), input.gender())
+ .reconnect(input.email(), input.phone())
+ .relocate(input.delivery(), input.billing())
+ .annotate(input.notes()))
+ .onFailure().transform(t -> Problem.valueOf(Status.BAD_REQUEST, t.getMessage()));
}
@ResponseStatus(204)
@PATCH
@Path("/{id}")
- public Uni modify(CustomerId id,
+ public Uni update(@RestPath CustomerId id,
@RestQuery Name firstname, @RestQuery Name lastname,
- @RestQuery Person.BirthDate birthdate, @RestQuery Person.Gender gender,
+ @RestQuery Person.Birth birth, @RestQuery Person.Gender gender,
@RestQuery EmailAddress email, @RestQuery PhoneNumber phone,
@RestQuery PhysicalAddress delivery, @RestQuery PhysicalAddress billing,
@RestQuery String notes) {
return service.modify(id, customer -> {
if (firstname != null || lastname != null) customer.rename(firstname, lastname);
- if (birthdate != null || gender != null) customer.redefine(birthdate, gender);
+ if (birth != null || gender != null) customer.redefine(birth, gender);
if (email != null || phone != null) customer.reconnect(email, phone);
if (delivery != null || billing != null) customer.relocate(delivery, billing);
if (notes != null) customer.annotate(notes);
- });
+ }).onFailure().transform(t -> Problem.valueOf(Status.BAD_REQUEST, t.getMessage()));
}
@ResponseStatus(204)
- @PUT
+ @PATCH
@Path("/{id}/naming")
- public Uni rename(CustomerId id, @RestQuery Name firstname, @RestQuery Name lastname) {
- return service.modify(id, customer -> customer.rename(firstname, lastname));
+ public Uni rename(@RestPath CustomerId id, @RestQuery Name firstname, @RestQuery Name lastname) {
+ return service.modify(id, customer -> customer.rename(firstname, lastname))
+ .onFailure().transform(t -> Problem.valueOf(Status.BAD_REQUEST, t.getMessage()));
}
@ResponseStatus(204)
- @PUT
+ @PATCH
@Path("/{id}/definition")
- public Uni redefine(CustomerId id, @RestQuery Person.BirthDate birthdate, @RestQuery Person.Gender gender) {
- return service.modify(id, customer -> customer.redefine(birthdate, gender));
+ public Uni redefine(@RestPath CustomerId id, @RestQuery Person.Birth birth, @RestQuery Person.Gender gender) {
+ return service.modify(id, customer -> customer.redefine(birth, gender))
+ .onFailure().transform(t -> Problem.valueOf(Status.BAD_REQUEST, t.getMessage()));
}
@ResponseStatus(204)
- @PUT
+ @PATCH
@Path("/{id}/connection")
- public Uni reconnect(CustomerId id, @RestQuery EmailAddress email, @RestQuery PhoneNumber phone) {
- return service.modify(id, customer -> customer.reconnect(email, phone));
+ public Uni reconnect(@RestPath CustomerId id, @RestQuery EmailAddress email, @RestQuery PhoneNumber phone) {
+ return service.modify(id, customer -> customer.reconnect(email, phone))
+ .onFailure().transform(t -> Problem.valueOf(Status.BAD_REQUEST, t.getMessage()));
}
@ResponseStatus(204)
- @PUT
+ @PATCH
@Path("/{id}/location")
- public Uni relocate(CustomerId id, @RestQuery PhysicalAddress delivery, @RestQuery PhysicalAddress billing) {
- return service.modify(id, customer -> customer.relocate(delivery, billing));
+ public Uni relocate(@RestPath CustomerId id, @RestQuery PhysicalAddress delivery, @RestQuery PhysicalAddress billing) {
+ return service.modify(id, customer -> customer.relocate(delivery, billing))
+ .onFailure().transform(t -> Problem.valueOf(Status.BAD_REQUEST, t.getMessage()));
}
@GET
@Path("/{id}")
- public Uni findOne(CustomerId id) {
+ public Uni get(@RestPath CustomerId id) {
return service.findOne(id)
.onFailure().transform(f -> Problem.valueOf(Status.NOT_FOUND, f.getMessage()))
.map(CustomerDto::from);
}
@GET
- public Multi findByCriteria(@RestQuery String filter) {
- return repository.findByCriteria(filter.isEmpty() ? FindCriteria.empty() : FindCriteria.from(filter))
- .map(CustomerDto::from);
+ public Uni> search(@RestQuery List firstname,
+ @RestQuery List lastname,
+ @RestQuery List birth,
+ @RestQuery List gender,
+ @RestQuery List email,
+ @RestQuery List phone) {
+
+ Map> criteria = new HashMap<>();
+ if (!firstname.isEmpty()) criteria.put("firstName", AttributeFilter.from(firstname, Name::new));
+ if (!lastname.isEmpty()) criteria.put("lastName", AttributeFilter.from(lastname, Name::new));
+ if (!birth.isEmpty()) criteria.put("birth", AttributeFilter.from(birth, Person.Birth::fromISO));
+ if (!gender.isEmpty()) criteria.put("gender", AttributeFilter.from(gender, Person.Gender::valueOf));
+ if (!email.isEmpty()) criteria.put("email", AttributeFilter.from(email, EmailAddress::new));
+ if (!phone.isEmpty()) criteria.put("phone", AttributeFilter.from(phone, PhoneNumber::new));
+
+ return repository.findByCriteria(criteria)
+ .map(l -> l.stream().map(CustomerDto::from).toList())
+ .onFailure().transform(t -> Problem.valueOf(Status.BAD_REQUEST, t.getMessage()));
}
@GET
@Path("/{id}/subscriptions")
- public Multi validSubscriptionsOf(CustomerId id, @RestQuery LocalDate at) {
+ public Multi validSubscriptionsOf(@RestPath CustomerId id, @RestQuery LocalDate at) {
return service.findOne(id)
.onFailure().transform(f -> Problem.valueOf(Status.NOT_FOUND, f.getMessage()))
.onItem().transformToMulti(s -> Multi.createFrom().iterable(s.subscriptionsAt(at)))
- .map(SubscriptionDto::from);
+ .map(SubscriptionDto::from)
+ .onFailure().transform(t -> Problem.valueOf(Status.BAD_REQUEST, t.getMessage()));
}
@ResponseStatus(204)
@DELETE
@Path("/{id}")
- public Uni remove(CustomerId id, @RestQuery Boolean force) {
- return service.remove(id, force != null && force);
+ public Uni remove(@RestPath CustomerId id, @RestQuery Boolean force) {
+ return service.remove(id, force != null && force)
+ .onFailure().transform(t -> Problem.valueOf(Status.BAD_REQUEST, t.getMessage()));
}
}
diff --git a/pta-customer/src/main/resources/application-dev.yaml b/pta-customer/src/main/resources/application-dev.yaml
new file mode 100644
index 0000000..bad4c54
--- /dev/null
+++ b/pta-customer/src/main/resources/application-dev.yaml
@@ -0,0 +1,9 @@
+quarkus:
+ management:
+ host: localhost
+ smallrye-openapi:
+ info-title: Customer API (development)
+ hibernate-orm:
+ database:
+ generation: drop-and-create
+
diff --git a/pta-customer/src/main/resources/application-test.yaml b/pta-customer/src/main/resources/application-test.yaml
new file mode 100644
index 0000000..d05820b
--- /dev/null
+++ b/pta-customer/src/main/resources/application-test.yaml
@@ -0,0 +1,10 @@
+quarkus:
+ log:
+ level: INFO
+ category:
+ org.hibernate.sql: DEBUG
+ hibernate-orm:
+ log:
+ sql: true
+ database:
+ generation: drop-and-create
diff --git a/pta-customer/src/main/resources/application.properties b/pta-customer/src/main/resources/application.properties
deleted file mode 100644
index d19422f..0000000
--- a/pta-customer/src/main/resources/application.properties
+++ /dev/null
@@ -1,21 +0,0 @@
-quarkus.quinoa.package-manager-install=true
-quarkus.quinoa.package-manager-install.node-version=20.10.0
-
-quarkus.swagger-ui.always-include=true
-
-quarkus.smallrye-openapi.info-title=Customer API
-%dev.quarkus.smallrye-openapi.info-title=Customer API (development)
-%test.quarkus.smallrye-openapi.info-title=Customer API (test)
-quarkus.smallrye-openapi.info-version=1.0.0
-quarkus.smallrye-openapi.info-description=Customer API
-quarkus.smallrye-openapi.info-terms-of-service=Your terms here
-quarkus.smallrye-openapi.info-contact-email=techsupport@ptagency.ch
-quarkus.smallrye-openapi.info-contact-name=Example API Support
-quarkus.smallrye-openapi.info-contact-url=https://ptagency.ch/contact
-quarkus.smallrye-openapi.info-license-name=Apache 2.0
-quarkus.smallrye-openapi.info-license-url=https://www.apache.org/licenses/LICENSE-2.0.html
-quarkus.smallrye-openapi.store-schema-directory=./doc
-
-quarkus.management.enabled=true
-%dev.quarkus.management.host=localhost
-quarkus.management.port=9000
diff --git a/pta-customer/src/main/resources/application.yaml b/pta-customer/src/main/resources/application.yaml
new file mode 100644
index 0000000..15bff8e
--- /dev/null
+++ b/pta-customer/src/main/resources/application.yaml
@@ -0,0 +1,27 @@
+quarkus:
+ datasource:
+ password: connor
+ username: sarah
+ db-kind: postgresql
+ management:
+ enabled: true
+ port: 9000
+ quinoa:
+ package-manager-install: true
+ package-manager-install:
+ node-version: 20.10.0
+ smallrye-openapi:
+ info-contact-email: techsupport@ptagency.ch
+ info-contact-name: Example API Support
+ info-contact-url: https://ptagency.ch/contact
+ info-description: Customer API
+ info-license-name: Apache 2.0
+ info-license-url: https://www.apache.org/licenses/LICENSE-2.0.html
+ info-terms-of-service: Your terms here
+ info-title: Customer API
+ info-version: 1.0.0
+ store-schema-directory: ./doc
+ swagger-ui:
+ always-include: true
+ jackson:
+ serialization-inclusion: non-null
diff --git a/pta-customer/src/test/java/ch/obya/pta/customer/application/CustomerServiceIT.java b/pta-customer/src/test/java/ch/obya/pta/customer/application/CustomerServiceIT.java
new file mode 100644
index 0000000..da4b1df
--- /dev/null
+++ b/pta-customer/src/test/java/ch/obya/pta/customer/application/CustomerServiceIT.java
@@ -0,0 +1,40 @@
+package ch.obya.pta.customer.application;
+
+import ch.obya.pta.common.application.EventPublisher;
+import ch.obya.pta.customer.domain.util.Samples;
+import io.quarkus.hibernate.reactive.panache.common.WithTransaction;
+import io.quarkus.test.InjectMock;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.vertx.RunOnVertxContext;
+import io.smallrye.mutiny.Uni;
+import io.smallrye.mutiny.helpers.test.UniAssertSubscriber;
+import jakarta.inject.Inject;
+import org.junit.jupiter.api.Test;
+
+import static org.mockito.ArgumentMatchers.anyCollection;
+import static org.mockito.Mockito.when;
+
+@QuarkusTest
+public class CustomerServiceIT {
+
+ @InjectMock
+ EventPublisher eventPublisher;
+ @Inject
+ CustomerService customerService;
+
+ @RunOnVertxContext
+ @Test
+ void create_customer_should_persist_and_publish_and_return_expected_id() {
+ var example = Samples.oneCustomer.get().state();
+
+ when(eventPublisher.publish(anyCollection())).thenReturn(Uni.createFrom().voidItem());
+
+ var result = customerService.create(
+ example.person(),
+ example.deliveryAddress(),
+ example.billingAddress(),
+ example.emailAddress(),
+ example.phoneNumber(),
+ example.notes()).subscribe().withSubscriber(UniAssertSubscriber.create()).getItem();
+ }
+}
diff --git a/pta-customer/src/test/java/ch/obya/pta/customer/application/CustomerServiceTest.java b/pta-customer/src/test/java/ch/obya/pta/customer/application/CustomerServiceTest.java
index d80f9a4..616cda1 100644
--- a/pta-customer/src/test/java/ch/obya/pta/customer/application/CustomerServiceTest.java
+++ b/pta-customer/src/test/java/ch/obya/pta/customer/application/CustomerServiceTest.java
@@ -74,7 +74,7 @@ void look_up_existing_customer_should_return_expected_entity() {
void create_customer_should_persist_and_publish_and_return_expected_id() {
var template = Samples.oneCustomer.get().state();
- when(customerRepository.findByCriteria(anyCollection())).thenReturn(Multi.createFrom().iterable(Collections.emptyList()));
+ when(customerRepository.findByCriteria(anyMap())).thenReturn(Uni.createFrom().item(Collections.emptyList()));
when(customerRepository.save(any()))
.thenAnswer(invocation -> Uni.createFrom().item(invocation.getArgument(0, Customer.class)));
when(eventPublisher.publish(anyCollection())).thenReturn(Uni.createFrom().voidItem());
@@ -101,8 +101,7 @@ void create_customer_should_persist_and_publish_and_return_expected_id() {
void create_duplicate_customer_should_fail() {
var template = Samples.oneCustomer.get().state();
- when(customerRepository.findByCriteria(anyCollection())).thenReturn(
- Multi.createFrom().iterable(List.of(Samples.oneCustomer.get())));
+ when(customerRepository.findByCriteria(anyMap())).thenReturn(Uni.createFrom().item(List.of(Samples.oneCustomer.get())));
var result = customerService.create(
template.person(),
@@ -118,7 +117,7 @@ void create_duplicate_customer_should_fail() {
"Customer (%s,%s,%s,%s) already exists.".formatted(
template.person().firstName(),
template.person().lastName(),
- template.person().birthDate(),
+ template.person().birth(),
template.emailAddress()));
}
@@ -127,7 +126,7 @@ void modify_existing_customer_should_apply_and_persist_and_publish_and_return_ex
var customer = Samples.oneCustomer.get();
when(customerRepository.findOne(customer.id())).thenReturn(Uni.createFrom().item(customer));
- when(customerRepository.findByCriteria(anyCollection())).thenReturn(Multi.createFrom().iterable(Collections.emptyList()));
+ when(customerRepository.findByCriteria(anyMap())).thenReturn(Uni.createFrom().item(Collections.emptyList()));
when(customerRepository.save(any()))
.thenAnswer(invocation -> Uni.createFrom().item(invocation.getArgument(0, Customer.class)));
when(eventPublisher.publish(anyCollection())).thenReturn(Uni.createFrom().voidItem());
@@ -152,8 +151,7 @@ void modified_customer_conflicting_with_existing_one_should_fail() {
var state = customer.state();
when(customerRepository.findOne(customer.id())).thenReturn(Uni.createFrom().item(customer));
- when(customerRepository.findByCriteria(anyCollection())).thenReturn(
- Multi.createFrom().iterable(List.of(Samples.oneCustomer.get())));
+ when(customerRepository.findByCriteria(anyMap())).thenReturn(Uni.createFrom().item(List.of(Samples.oneCustomer.get())));
var result = customerService.modify(customer.id(), m -> m.annotate("modified").done());
@@ -163,7 +161,7 @@ void modified_customer_conflicting_with_existing_one_should_fail() {
"Customer (%s,%s,%s,%s) already exists.".formatted(
state.person().firstName(),
state.person().lastName(),
- state.person().birthDate(),
+ state.person().birth(),
state.emailAddress()));
}
diff --git a/pta-customer/src/test/java/ch/obya/pta/customer/domain/CustomerTest.java b/pta-customer/src/test/java/ch/obya/pta/customer/domain/CustomerTest.java
index 529cf05..fe168ed 100644
--- a/pta-customer/src/test/java/ch/obya/pta/customer/domain/CustomerTest.java
+++ b/pta-customer/src/test/java/ch/obya/pta/customer/domain/CustomerTest.java
@@ -1,19 +1,21 @@
package ch.obya.pta.customer.domain;
-import ch.obya.pta.common.domain.vo.Name;
import ch.obya.pta.common.util.validation.Checker;
import ch.obya.pta.customer.domain.event.CustomerCreated;
import ch.obya.pta.customer.domain.event.CustomerModified;
+import ch.obya.pta.customer.domain.util.Samples;
import ch.obya.pta.customer.domain.vo.EmailAddress;
import ch.obya.pta.customer.domain.vo.Person;
import ch.obya.pta.customer.domain.vo.PhoneNumber;
import ch.obya.pta.customer.domain.vo.PhysicalAddress;
-import ch.obya.pta.customer.domain.util.Samples;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import static ch.obya.pta.customer.domain.util.Samples.johnDoe;
+import static ch.obya.pta.customer.domain.util.Samples.sherlockHolmes;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
@@ -21,49 +23,49 @@ class CustomerTest {
@Test
void should_create_customer_and_apply_fast_checks_and_emit_events() {
- var customer = Samples.oneCustomer.get();
- var state = customer.state();
+ var johnDoe = Samples.johnDoe();
+ var state = johnDoe.state();
assertThat(state.person().firstName().content()).isEqualTo("John");
assertThat(state.person().lastName().content()).isEqualTo("Doe");
- assertThat(state.person().birthDate().date()).isEqualTo(LocalDate.of(1966, 4, 5));
+ assertThat(state.person().birth().date()).isEqualTo(LocalDate.of(1966, 4, 5));
assertThat(state.person().gender()).isEqualTo(Person.Gender.MALE);
assertThat(state.emailAddress()).isEqualTo(new EmailAddress("john@doe.ch"));
assertThat(state.phoneNumber()).isEqualTo(new PhoneNumber("+41 78 123 45 67"));
assertThat(state.deliveryAddress().streetNo()).isEqualTo("test-delivery");
assertThat(state.billingAddress().streetNo()).isEqualTo("test-billing");
- assertThat(state.notes()).isEqualTo("notes");
+ assertThat(state.notes()).isEqualTo("no notes");
- assertThat(customer.domainEvents())
+ assertThat(johnDoe.domainEvents())
.usingRecursiveFieldByFieldElementComparatorIgnoringFields("timestamp")
- .containsExactly(new CustomerCreated(customer.id()));
- assertThat(customer.domainEvents()).isEmpty();
+ .containsExactly(new CustomerCreated(johnDoe.id()));
+ assertThat(johnDoe.domainEvents()).isEmpty();
}
@Test
void should_apply_modifications_and_apply_fast_checks_and_emit_events() {
- var customer = Samples.oneCustomer.get();
- var state = customer.state();
+ var customer = johnDoe();
+ var sherlockHolmes = sherlockHolmes().state();
customer.modify()
- .rename(new Name("albert"), new Name("einstein"))
- .redefine(new Person.BirthDate(LocalDate.of(1958, 12,12)), Person.Gender.MALE)
- .reconnect(new EmailAddress("albert@einstein.com"), new PhoneNumber("+33123456789"))
- .relocate(new PhysicalAddress("Bakerstreet 22", "89000", "London", "London", "UK"), state.deliveryAddress())
- .annotate("modified")
+ .rename(sherlockHolmes.person().firstName(), sherlockHolmes.person().lastName())
+ .redefine(sherlockHolmes.person().birth(), sherlockHolmes.person().gender())
+ .reconnect(sherlockHolmes.emailAddress(), sherlockHolmes.phoneNumber())
+ .relocate(sherlockHolmes.deliveryAddress(), sherlockHolmes.billingAddress())
+ .annotate("modified by Sherlock Holmes")
.done();
- state = customer.state();
+ var state = customer.state();
- assertThat(state.person().firstName().content()).isEqualTo("Albert");
- assertThat(state.person().lastName().content()).isEqualTo("Einstein");
- assertThat(state.person().birthDate().date()).isEqualTo(LocalDate.of(1958, 12,12));
- assertThat(state.person().gender()).isEqualTo(Person.Gender.MALE);
- assertThat(state.emailAddress()).isEqualTo(new EmailAddress("albert@einstein.com"));
- assertThat(state.phoneNumber()).isEqualTo(new PhoneNumber("+33 1 23 45 67 89"));
- assertThat(state.deliveryAddress().streetNo()).isEqualTo("Bakerstreet 22");
- assertThat(state.billingAddress().streetNo()).isEqualTo("test-delivery");
- assertThat(state.notes()).isEqualTo("modified");
+ assertThat(state.person().firstName().content()).isEqualTo(sherlockHolmes.person().firstName().content());
+ assertThat(state.person().lastName().content()).isEqualTo(sherlockHolmes.person().lastName().content());
+ assertThat(state.person().birth().date()).isEqualTo(sherlockHolmes.person().birth().date());
+ assertThat(state.person().gender()).isEqualTo(sherlockHolmes.person().gender());
+ assertThat(state.emailAddress().address()).isEqualTo(sherlockHolmes.emailAddress().address());
+ assertThat(state.phoneNumber().number()).isEqualTo(sherlockHolmes.phoneNumber().number());
+ assertThat(state.deliveryAddress().streetNo()).isEqualTo(sherlockHolmes.deliveryAddress().streetNo());
+ assertThat(state.billingAddress().streetNo()).isEqualTo(sherlockHolmes.billingAddress().streetNo());
+ assertThat(state.notes()).isEqualTo("modified by Sherlock Holmes");
assertThat(customer.domainEvents())
.usingRecursiveFieldByFieldElementComparatorIgnoringFields("timestamp")
@@ -75,7 +77,7 @@ void should_apply_modifications_and_apply_fast_checks_and_emit_events() {
@Test
void should_apply_given_address_checker_on_actual_state() {
- var customer = Samples.oneCustomer.get();
+ var customer = Samples.johnDoe();
var checker = mock(TestAddressChecker.class);
customer.validateWith(checker);
@@ -84,5 +86,10 @@ void should_apply_given_address_checker_on_actual_state() {
assertThat(captor.getAllValues()).extracting(PhysicalAddress::streetNo).containsExactly("test-delivery", "test-billing");
}
+ @Test
+ void test() {
+ LocalDate.parse("1958-12-12", DateTimeFormatter.ISO_LOCAL_DATE);
+ }
+
private interface TestAddressChecker extends Checker {}
}
diff --git a/pta-customer/src/test/java/ch/obya/pta/customer/infrastructure/data/PanacheCustomerEntityTest.java b/pta-customer/src/test/java/ch/obya/pta/customer/infrastructure/data/PanacheCustomerEntityTest.java
new file mode 100644
index 0000000..b70ced3
--- /dev/null
+++ b/pta-customer/src/test/java/ch/obya/pta/customer/infrastructure/data/PanacheCustomerEntityTest.java
@@ -0,0 +1,28 @@
+package ch.obya.pta.customer.infrastructure.data;
+
+import io.quarkus.test.hibernate.reactive.panache.TransactionalUniAsserter;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.vertx.RunOnVertxContext;
+import org.junit.jupiter.api.Test;
+
+import java.util.UUID;
+
+@QuarkusTest
+public class PanacheCustomerEntityTest {
+
+ @RunOnVertxContext
+ @Test
+ void should_find_customer(TransactionalUniAsserter asserter) {
+ asserter.execute(() -> PanacheCustomerEntity
+ .find("logicalId", UUID.fromString("f1b9b1b1-1b1b-1b1b-1b1b-1b1b1b1b1b1b"))
+ .firstResult());
+ }
+
+ @RunOnVertxContext
+ @Test
+ void should_find_customers(TransactionalUniAsserter asserter) {
+ asserter.execute(() -> PanacheCustomerEntity
+ .find("from customer where lastName = 'Doe' and birth = {d '1966-04-05'} and email = 'john@doe.ch' and firstName = 'John'")
+ .list());
+ }
+}
diff --git a/pta-customer/src/test/java/ch/obya/pta/customer/infrastructure/web/CustomerResourceIT.java b/pta-customer/src/test/java/ch/obya/pta/customer/infrastructure/web/CustomerResourceIT.java
new file mode 100644
index 0000000..21280ee
--- /dev/null
+++ b/pta-customer/src/test/java/ch/obya/pta/customer/infrastructure/web/CustomerResourceIT.java
@@ -0,0 +1,229 @@
+package ch.obya.pta.customer.infrastructure.web;
+
+import ch.obya.pta.customer.domain.vo.CustomerId;
+import io.quarkus.test.common.http.TestHTTPEndpoint;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.vertx.RunOnVertxContext;
+import io.restassured.http.ContentType;
+import org.junit.jupiter.api.*;
+
+import java.util.Map;
+
+import static ch.obya.pta.customer.domain.util.Samples.johnDoe;
+import static ch.obya.pta.customer.domain.util.Samples.sherlockHolmes;
+import static io.restassured.RestAssured.given;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+
+
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+@TestHTTPEndpoint(CustomerResource.class)
+@QuarkusTest
+class CustomerResourceIT {
+
+ final CustomerDto johnDoe = CustomerDto.from(johnDoe()).toBuilder().id(null).build();
+ final CustomerDto sherlockHolmes = CustomerDto.from(sherlockHolmes()).toBuilder().id(null).build();
+
+ static CustomerId id;
+
+ @Order(1)
+ @RunOnVertxContext
+ @Test
+ void should_create_customer() {
+ assertThat(id = given()
+ .contentType(ContentType.JSON)
+ .body(johnDoe)
+ .when()
+ .post()
+ .then()
+ .log().all()
+ .statusCode(201)
+ .extract().jsonPath().getObject("", CustomerId.class)).isNotNull();
+ assertThat(id.id()).isNotNull();
+ }
+
+ @Order(2)
+ @RunOnVertxContext
+ @Test
+ void should_get_customer() {
+ assertThat(id).isNotNull();
+ assertThat(
+ given()
+ .when()
+ .get("/{id}", Map.of("id", id.id()))
+ .then()
+ .log().all()
+ .statusCode(200)
+ .extract().jsonPath().getObject("", CustomerDto.class))
+ .extracting(CustomerDto::firstName, CustomerDto::lastName)
+ .containsExactly(johnDoe.firstName(), johnDoe.lastName());
+ }
+
+ @Order(3)
+ @RunOnVertxContext
+ @Test
+ void should_list_customers() {
+ assertThat(
+ given()
+ .when()
+ .get()
+ .then()
+ .log().all()
+ .statusCode(200)
+ .extract().jsonPath().getList("", CustomerDto.class))
+ .extracting(CustomerDto::firstName, CustomerDto::lastName)
+ .contains(tuple(johnDoe.firstName(), johnDoe.lastName()));
+ }
+
+ @Order(4)
+ @RunOnVertxContext
+ @Test
+ void should_search_customers() {
+ assertThat(
+ given()
+ .when()
+ .queryParams("firstName", "ne:jude")
+ .queryParams("lastName", "eq:Doe")
+ .queryParams("birth", "eq:1966-04-05")
+ .queryParams("gender", "eq:MALE")
+ .get()
+ .then()
+ .log().all()
+ .statusCode(200)
+ .extract().jsonPath().getList("", CustomerDto.class))
+ .extracting(CustomerDto::firstName, CustomerDto::lastName)
+ .contains(tuple(johnDoe.firstName(), johnDoe.lastName()));
+ }
+
+ @Order(5)
+ @RunOnVertxContext
+ @Test
+ void should_replace_customer() {
+ given()
+ .contentType(ContentType.JSON)
+ .body(sherlockHolmes)
+ .when()
+ .put("/{id}", Map.of("id", id.id()))
+ .then()
+ .log().all()
+ .statusCode(204);
+
+ //get
+ assertThat(
+ given()
+ .when()
+ .get("/{id}", Map.of("id", id.id()))
+ .then()
+ .log().all()
+ .statusCode(200)
+ .extract().jsonPath().getObject("", CustomerDto.class))
+ .extracting(
+ CustomerDto::firstName,
+ CustomerDto::lastName,
+ CustomerDto::birth,
+ CustomerDto::delivery)
+ .containsExactly(
+ sherlockHolmes.firstName(),
+ sherlockHolmes.lastName(),
+ sherlockHolmes.birth(),
+ sherlockHolmes.delivery());
+ }
+
+ @Order(6)
+ @RunOnVertxContext
+ @Test
+ void should_update_customer() {
+ given()
+ .when()
+ .queryParams("email", johnDoe.email().address())
+ .queryParams("phone", johnDoe.phone().number())
+ .patch("/{id}", Map.of("id", id.id()))
+ .then()
+ .log().all()
+ .statusCode(204);
+
+ //update
+ given()
+ .when()
+ .queryParams("firstName", johnDoe.firstName().content(), "lastName", johnDoe.lastName().content())
+ .patch("/{id}/naming", Map.of("id", id.id()))
+ .then()
+ .log().all()
+ .statusCode(204);
+
+ //update
+ given()
+ .when()
+ .queryParams("birth", johnDoe.birth().toISO())
+ .queryParams("gender", johnDoe.gender())
+ .patch("/{id}/definition", Map.of("id", id.id()))
+ .then()
+ .log().all()
+ .statusCode(204);
+
+ //update
+ given()
+ .when()
+ .queryParams("email", johnDoe.email().address())
+ .queryParams("phone", johnDoe.phone().number())
+ .patch("/{id}/connection", Map.of("id", id.id()))
+ .then()
+ .log().all()
+ .statusCode(204);
+
+ //update
+ var delivery = "%s,%s,%s,%s,%s".formatted(johnDoe.delivery().streetNo(), johnDoe.delivery().zip(), johnDoe.delivery().city(), johnDoe.delivery().region(), johnDoe.delivery().country());
+ given()
+ .when()
+ .queryParams("delivery", delivery)
+ .patch("/{id}/location", Map.of("id", id.id()))
+ .then()
+ .log().all()
+ .statusCode(204);
+
+ //get
+ assertThat(
+ given()
+ .when()
+ .get("/{id}", Map.of("id", id.id()))
+ .then()
+ .log().all()
+ .statusCode(200)
+ .extract().jsonPath().getObject("", CustomerDto.class))
+ .extracting(
+ CustomerDto::firstName,
+ CustomerDto::lastName,
+ CustomerDto::birth,
+ CustomerDto::delivery,
+ CustomerDto::email,
+ CustomerDto::phone)
+ .containsExactly(
+ johnDoe.firstName(),
+ johnDoe.lastName(),
+ johnDoe.birth(),
+ johnDoe.delivery(),
+ johnDoe.email(),
+ johnDoe.phone());
+ }
+
+ @Order(7)
+ @RunOnVertxContext
+ @Test
+ void should_remove_customer() {
+ //remove
+ given()
+ .when()
+ .queryParams("force", "true")
+ .delete("/{id}", Map.of("id", id.id()))
+ .then()
+ .log().all()
+ .statusCode(204);
+
+ given()
+ .when()
+ .get("/{id}", Map.of("id", id.id()))
+ .then()
+ .log().all()
+ .statusCode(404);
+ }
+}
\ No newline at end of file
diff --git a/pta-customer/src/test/java/ch/obya/pta/customer/infrastructure/web/CustomerResourceTest.java b/pta-customer/src/test/java/ch/obya/pta/customer/infrastructure/web/CustomerResourceTest.java
index c264018..b3ebd6f 100644
--- a/pta-customer/src/test/java/ch/obya/pta/customer/infrastructure/web/CustomerResourceTest.java
+++ b/pta-customer/src/test/java/ch/obya/pta/customer/infrastructure/web/CustomerResourceTest.java
@@ -18,8 +18,7 @@
import java.util.Map;
import java.util.function.Consumer;
-import static ch.obya.pta.customer.domain.util.Samples.oneCustomer;
-import static ch.obya.pta.customer.domain.util.Samples.oneCustomerWithOneYearSubscription;
+import static ch.obya.pta.customer.domain.util.Samples.*;
import static io.restassured.RestAssured.given;
import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;
import static org.assertj.core.api.Assertions.assertThat;
@@ -107,19 +106,19 @@ void create_customer_resource_should_succeed_with_201() {
verify(customerService).create(personCaptor.capture(), any(), any(), any(), any(), any());
assertThat(personCaptor.getValue().firstName()).isEqualTo(dto.firstName());
assertThat(personCaptor.getValue().lastName()).isEqualTo(dto.lastName());
- assertThat(personCaptor.getValue().birthDate()).isEqualTo(dto.birthDate());
+ assertThat(personCaptor.getValue().birth()).isEqualTo(dto.birth());
assertThat(personCaptor.getValue().gender()).isEqualTo(dto.gender());
}
@Test
- void modify_existing_customer_resource_should_succeed_with_204() {
+ void replace_existing_customer_resource_should_succeed_with_204() {
var customer = oneCustomer.get();
when(customerService.modify(any(), any())).thenReturn(Uni.createFrom().voidItem());
var dto = new CustomerDto.Builder()
.firstName(new Name("albert"))
.lastName(new Name("einstein"))
- .birthDate(new Person.BirthDate(LocalDate.of(1958, 12, 12)))
+ .birth(new Person.Birth(LocalDate.of(1958, 12, 12)))
.gender(Person.Gender.MALE)
.build();
@@ -134,19 +133,19 @@ void modify_existing_customer_resource_should_succeed_with_204() {
verifyModifier(customer, m -> {
verify(m).rename(new Name("albert"), new Name("einstein"));
- verify(m).redefine(new Person.BirthDate(LocalDate.of(1958, 12, 12)), Person.Gender.MALE);
+ verify(m).redefine(new Person.Birth(LocalDate.of(1958, 12, 12)), Person.Gender.MALE);
});
}
@Test
- void partly_modify_existing_customer_resource_should_succeed_with_204() {
- var customer = oneCustomer.get();
+ void partly_update_existing_customer_resource_should_succeed_with_204() {
+ var customer = johnDoe();
when(customerService.modify(any(), any())).thenReturn(Uni.createFrom().voidItem());
given()
.when()
.queryParams("firstName", "albert", "lastName", "einstein")
- .queryParams("birthDate", "1958-12-12", "gender", "MALE")
+ .queryParams("birth", "1958-12-12", "gender", "MALE")
.patch("/{id}", Map.of("id", customer.id().id()))
.then()
.log().all()
@@ -154,7 +153,7 @@ void partly_modify_existing_customer_resource_should_succeed_with_204() {
verifyModifier(customer, m -> {
verify(m).rename(new Name("albert"), new Name("einstein"));
- verify(m).redefine(new Person.BirthDate(LocalDate.of(1958, 12, 12)), Person.Gender.MALE);
+ verify(m).redefine(new Person.Birth(LocalDate.of(1958, 12, 12)), Person.Gender.MALE);
});
}
@@ -166,7 +165,7 @@ void rename_existing_customer_resource_should_succeed_with_204() {
given()
.when()
.queryParams("firstName", "albert", "lastName", "einstein")
- .put("/{id}/naming", Map.of("id", customer.id().id()))
+ .patch("/{id}/naming", Map.of("id", customer.id().id()))
.then()
.log().all()
.statusCode(204);
@@ -182,14 +181,14 @@ void redefine_existing_customer_resource_should_succeed_with_204() {
given()
.when()
- .queryParams("birthDate", "1958-12-12", "gender", "MALE")
- .put("/{id}/definition", Map.of("id", customer.id().id()))
+ .queryParams("birth", "1958-12-12", "gender", "MALE")
+ .patch("/{id}/definition", Map.of("id", customer.id().id()))
.then()
.log().all()
.statusCode(204);
verifyModifier(customer, m -> verify(m)
- .redefine(new Person.BirthDate(LocalDate.of(1958, 12,12)), Person.Gender.MALE));
+ .redefine(new Person.Birth(LocalDate.of(1958, 12,12)), Person.Gender.MALE));
}
@Test
@@ -200,7 +199,7 @@ void reconnect_existing_customer_resource_should_succeed_with_204() {
given()
.when()
.queryParams("email", "albert@einstein.com", "phone", "+33123456789")
- .put("/{id}/connection", Map.of("id", customer.id().id()))
+ .patch("/{id}/connection", Map.of("id", customer.id().id()))
.then()
.log().all()
.statusCode(204);
@@ -217,7 +216,7 @@ void relocate_existing_customer_resource_should_succeed_with_204() {
given()
.when()
.queryParams("delivery", "Bakerstreet 22,89000,London,London,UK")
- .put("/{id}/location", Map.of("id", customer.id().id()))
+ .patch("/{id}/location", Map.of("id", customer.id().id()))
.then()
.log().all()
.statusCode(204);
diff --git a/pta-customer/src/test/java/ch/obya/pta/customer/util/search/CustomerAttributeFilterTest.java b/pta-customer/src/test/java/ch/obya/pta/customer/util/search/CustomerAttributeFilterTest.java
new file mode 100644
index 0000000..083a331
--- /dev/null
+++ b/pta-customer/src/test/java/ch/obya/pta/customer/util/search/CustomerAttributeFilterTest.java
@@ -0,0 +1,24 @@
+package ch.obya.pta.customer.util.search;
+
+import ch.obya.pta.common.domain.vo.Name;
+import ch.obya.pta.common.util.search.AttributeFilter;
+import ch.obya.pta.customer.domain.vo.EmailAddress;
+import ch.obya.pta.customer.domain.vo.Person;
+import ch.obya.pta.customer.domain.vo.PhoneNumber;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDate;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CustomerAttributeFilterTest {
+
+ @Test
+ void should_build_attribute_filter() {
+ assertThat(AttributeFilter.from("eq:albert", Name::new)).isEqualTo(AttributeFilter.equal(new Name("albert")));
+ assertThat(AttributeFilter.from("eq:1958-12-12", Person.Birth::fromISO)).isEqualTo(AttributeFilter.equal(new Person.Birth(LocalDate.of(1958,12,12))));
+ assertThat(AttributeFilter.from("eq:MALE", Person.Gender::valueOf)).isEqualTo(AttributeFilter.equal(Person.Gender.MALE));
+ assertThat(AttributeFilter.from("eq:albert@einstein.ch", EmailAddress::new)).isEqualTo(AttributeFilter.equal(new EmailAddress("albert@einstein.ch")));
+ assertThat(AttributeFilter.from("eq:0041781234567", PhoneNumber::new)).isEqualTo(AttributeFilter.equal(new PhoneNumber("0041781234567")));
+ }
+}