mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 08:29:36 +01:00
refactor: EntityDraft-Pattern auf Customer, Article und ProductCategory anwenden
- CustomerDraft / CustomerUpdateDraft eingeführt - ArticleDraft / ArticleUpdateDraft eingeführt - ProductCategoryDraft / ProductCategoryUpdateDraft eingeführt - Customer.create() nimmt jetzt CustomerDraft, gibt Result zurück - Customer.update(CustomerUpdateDraft) ersetzt 4× updateXxx(VO) - Article.create() nimmt jetzt ArticleDraft statt VOs - Article.update(ArticleUpdateDraft) ersetzt rename() + changeCategory() - ProductCategory.create() nimmt jetzt ProductCategoryDraft, gibt Result zurück - ProductCategory.update(ProductCategoryUpdateDraft) ersetzt rename() + updateDescription() - Use Cases bauen Draft aus Command, kein VO-Wissen im Application Layer - CreateCustomerCommand / UpdateCustomerCommand: int → Integer für paymentDueDays - CLAUDE.md: EntityDraft-Pattern-Dokumentation ergänzt
This commit is contained in:
parent
6b341f217b
commit
87123df2e4
30 changed files with 625 additions and 329 deletions
26
CLAUDE.md
26
CLAUDE.md
|
|
@ -23,6 +23,8 @@ Bounded Contexts: `usermanagement` (implementiert), `production`, `quality`, `in
|
||||||
| Command | `{Verb}{Noun}Command` | `CreateUserCommand` |
|
| Command | `{Verb}{Noun}Command` | `CreateUserCommand` |
|
||||||
| Domain Entity | `{Noun}` | `User`, `Role` |
|
| Domain Entity | `{Noun}` | `User`, `Role` |
|
||||||
| Value Object | `{Noun}` | `UserId`, `PasswordHash`, `RoleName` |
|
| Value Object | `{Noun}` | `UserId`, `PasswordHash`, `RoleName` |
|
||||||
|
| Create-Draft | `{Noun}Draft` | `SupplierDraft` |
|
||||||
|
| Update-Draft | `{Noun}UpdateDraft` | `SupplierUpdateDraft` |
|
||||||
| Domain Error | `{Noun}Error` (sealed interface) | `UserError.UsernameAlreadyExists` |
|
| Domain Error | `{Noun}Error` (sealed interface) | `UserError.UsernameAlreadyExists` |
|
||||||
| JPA Entity | `{Noun}Entity` | `UserEntity` |
|
| JPA Entity | `{Noun}Entity` | `UserEntity` |
|
||||||
| Mapper | `{Noun}Mapper` | `UserMapper` (Domain↔JPA) |
|
| Mapper | `{Noun}Mapper` | `UserMapper` (Domain↔JPA) |
|
||||||
|
|
@ -32,6 +34,30 @@ Bounded Contexts: `usermanagement` (implementiert), `production`, `quality`, `in
|
||||||
| Web DTO | `{Verb}{Noun}Request` | `CreateUserRequest` |
|
| Web DTO | `{Verb}{Noun}Request` | `CreateUserRequest` |
|
||||||
| Action Enum | `{Noun}Action implements Action` | `ProductionAction` |
|
| Action Enum | `{Noun}Action implements Action` | `ProductionAction` |
|
||||||
|
|
||||||
|
## EntityDraft-Pattern
|
||||||
|
|
||||||
|
Für Aggregate mit komplexer VO-Konstruktion (Address, ContactInfo, PaymentTerms u.ä.) gilt:
|
||||||
|
Der Application Layer baut **keine** VOs – er erzeugt einen **Draft-Record** mit rohen Strings
|
||||||
|
und übergibt ihn ans Aggregate. Das Aggregate orchestriert Validierung und VO-Konstruktion intern.
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Application Layer – nur Daten weitergeben, kein VO-Wissen
|
||||||
|
var draft = new SupplierDraft(cmd.name(), cmd.phone(), ...);
|
||||||
|
switch (Supplier.create(draft)) { ... }
|
||||||
|
|
||||||
|
// Domain Layer – validiert intern, gibt Result zurück
|
||||||
|
public static Result<SupplierError, Supplier> create(SupplierDraft draft) { ... }
|
||||||
|
public Result<SupplierError, Void> update(SupplierUpdateDraft draft) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Regeln:**
|
||||||
|
- Pflichtfelder: non-null im Draft-Record
|
||||||
|
- Optionale VOs (z.B. Address, PaymentTerms): `null`-Felder → VO wird nicht konstruiert
|
||||||
|
- Primitive `int` → `Integer` wenn das Feld optional/nullable sein muss
|
||||||
|
- Einzelne `updateXxx(VO)`-Methoden entfallen → ersetzt durch ein `update({Noun}UpdateDraft)`
|
||||||
|
- Uniqueness-Check bleibt im Application Layer (Repository-Concern), nach `Aggregate.create()`
|
||||||
|
- Invarianten-Kommentar im Aggregat aktuell halten
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
Funktional via `Result<E, T>` (`shared.common.Result`). Domain-Fehler sind sealed interfaces mit Records. Keine Exceptions im Domain/Application Layer.
|
Funktional via `Result<E, T>` (`shared.common.Result`). Domain-Fehler sind sealed interfaces mit Records. Keine Exceptions im Domain/Application Layer.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ VALUES (
|
||||||
'admin-001',
|
'admin-001',
|
||||||
'admin',
|
'admin',
|
||||||
'admin@effigenix.com',
|
'admin@effigenix.com',
|
||||||
'$2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYKKHFw3zqm', -- "admin123"
|
'$2a$12$SJmX80hUZoA66W77CX7cHeRw1TPscXD6S8HYEZfhJ5PxTfkbwbLdi', -- "admin123"
|
||||||
NULL, -- Kein Branch = globaler Zugriff
|
NULL, -- Kein Branch = globaler Zugriff
|
||||||
'ACTIVE',
|
'ACTIVE',
|
||||||
CURRENT_TIMESTAMP,
|
CURRENT_TIMESTAMP,
|
||||||
|
|
@ -127,7 +127,33 @@ curl -X POST http://localhost:8080/api/users \
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
## 6. Datenbank erkunden
|
## 6. BCrypt-Hash generieren
|
||||||
|
|
||||||
|
Beim Anlegen von Seed-Daten (SQL-Migrations) werden BCrypt-Hashes benötigt. Da verschiedene Tools unterschiedliche Hashes erzeugen (gleiche Ausgabe, verschiedenes Salt – das ist gewollt), muss der Hash **mit Spring's BCryptPasswordEncoder** generiert werden, damit er zur App-Konfiguration passt.
|
||||||
|
|
||||||
|
> **Wichtig:** Niemals einen Hash aus einem anderen Tool (htpasswd, online-Generatoren etc.) in Seed-Daten verwenden – nur Spring-generierte Hashes verifizieren korrekt.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Hash für ein Passwort generieren (Strength 12 wie in der App konfiguriert)
|
||||||
|
SPRING_CRYPTO=$(find ~/.m2/repository/org/springframework/security -name "spring-security-crypto-*.jar" | tail -1)
|
||||||
|
COMMONS=$(find ~/.m2/repository/commons-logging -name "*.jar" | tail -1)
|
||||||
|
|
||||||
|
cat > /tmp/GenHash.java << 'EOF'
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
public class GenHash {
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
String password = args.length > 0 ? args[0] : "admin123";
|
||||||
|
BCryptPasswordEncoder enc = new BCryptPasswordEncoder(12);
|
||||||
|
System.out.println(enc.encode(password));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
javac -cp "$SPRING_CRYPTO:$COMMONS" /tmp/GenHash.java -d /tmp
|
||||||
|
java -cp "/tmp:$SPRING_CRYPTO:$COMMONS" GenHash "meinPasswort"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Datenbank erkunden
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# PostgreSQL CLI
|
# PostgreSQL CLI
|
||||||
|
|
@ -141,7 +167,7 @@ SELECT * FROM role_permissions; -- Rollen-Permissions
|
||||||
SELECT * FROM audit_logs ORDER BY timestamp DESC LIMIT 10; -- Letzte 10 Audit Logs
|
SELECT * FROM audit_logs ORDER BY timestamp DESC LIMIT 10; -- Letzte 10 Audit Logs
|
||||||
```
|
```
|
||||||
|
|
||||||
## 7. Development Workflow
|
## 8. Development Workflow
|
||||||
|
|
||||||
### Code-Änderungen testen
|
### Code-Änderungen testen
|
||||||
|
|
||||||
|
|
@ -166,7 +192,7 @@ mvn verify
|
||||||
|
|
||||||
Bereits in `pom.xml` enthalten - Code-Änderungen werden automatisch neu geladen.
|
Bereits in `pom.xml` enthalten - Code-Änderungen werden automatisch neu geladen.
|
||||||
|
|
||||||
## 8. Typische Entwicklungs-Szenarien
|
## 9. Typische Entwicklungs-Szenarien
|
||||||
|
|
||||||
### Neuen User erstellen (via API)
|
### Neuen User erstellen (via API)
|
||||||
|
|
||||||
|
|
@ -201,7 +227,7 @@ PUT /api/users/{userId}/password
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 9. Fehlersuche
|
## 10. Fehlersuche
|
||||||
|
|
||||||
### Port 8080 bereits belegt
|
### Port 8080 bereits belegt
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -227,7 +253,7 @@ mvn flyway:info
|
||||||
mvn flyway:repair
|
mvn flyway:repair
|
||||||
```
|
```
|
||||||
|
|
||||||
## 10. Nächste Schritte
|
## 11. Nächste Schritte
|
||||||
|
|
||||||
- 📖 Lies [USER_MANAGEMENT.md](./USER_MANAGEMENT.md) für Details
|
- 📖 Lies [USER_MANAGEMENT.md](./USER_MANAGEMENT.md) für Details
|
||||||
- 🧪 Schreibe Integration Tests
|
- 🧪 Schreibe Integration Tests
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,10 @@ package de.effigenix.application.masterdata;
|
||||||
|
|
||||||
import de.effigenix.application.masterdata.command.CreateArticleCommand;
|
import de.effigenix.application.masterdata.command.CreateArticleCommand;
|
||||||
import de.effigenix.domain.masterdata.*;
|
import de.effigenix.domain.masterdata.*;
|
||||||
import de.effigenix.shared.common.Money;
|
|
||||||
import de.effigenix.shared.common.Result;
|
import de.effigenix.shared.common.Result;
|
||||||
import de.effigenix.shared.security.ActorId;
|
import de.effigenix.shared.security.ActorId;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import static de.effigenix.shared.common.Result.*;
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public class CreateArticle {
|
public class CreateArticle {
|
||||||
|
|
||||||
|
|
@ -19,50 +16,31 @@ public class CreateArticle {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result<ArticleError, Article> execute(CreateArticleCommand cmd, ActorId performedBy) {
|
public Result<ArticleError, Article> execute(CreateArticleCommand cmd, ActorId performedBy) {
|
||||||
ArticleNumber articleNumber;
|
var draft = new ArticleDraft(
|
||||||
switch (ArticleNumber.of(cmd.articleNumber())) {
|
cmd.name(), cmd.articleNumber(), cmd.categoryId(),
|
||||||
case Failure(var msg) -> { return Result.failure(new ArticleError.ValidationFailure(msg)); }
|
cmd.unit(), cmd.priceModel(), cmd.price()
|
||||||
case Success(var val) -> articleNumber = val;
|
);
|
||||||
|
|
||||||
|
Article article;
|
||||||
|
switch (Article.create(draft)) {
|
||||||
|
case Result.Failure(var err) -> { return Result.failure(err); }
|
||||||
|
case Result.Success(var val) -> article = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (articleRepository.existsByArticleNumber(articleNumber)) {
|
switch (articleRepository.existsByArticleNumber(article.articleNumber())) {
|
||||||
case Failure(var err) ->
|
case Result.Failure(var err) ->
|
||||||
{ return Result.failure(new ArticleError.RepositoryFailure(err.message())); }
|
{ return Result.failure(new ArticleError.RepositoryFailure(err.message())); }
|
||||||
case Success(var exists) -> {
|
case Result.Success(var exists) -> {
|
||||||
if (exists) {
|
if (exists) {
|
||||||
return Result.failure(new ArticleError.ArticleNumberAlreadyExists(cmd.articleNumber()));
|
return Result.failure(new ArticleError.ArticleNumberAlreadyExists(cmd.articleNumber()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Money price;
|
|
||||||
switch (Money.tryEuro(cmd.price())) {
|
|
||||||
case Failure(var msg) -> { return Result.failure(new ArticleError.ValidationFailure(msg)); }
|
|
||||||
case Success(var val) -> price = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
SalesUnit salesUnit;
|
|
||||||
switch (SalesUnit.create(cmd.unit(), cmd.priceModel(), price)) {
|
|
||||||
case Failure(var err) -> { return Result.failure(err); }
|
|
||||||
case Success(var val) -> salesUnit = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
ArticleName name;
|
|
||||||
switch (ArticleName.of(cmd.name())) {
|
|
||||||
case Failure(var msg) -> { return Result.failure(new ArticleError.ValidationFailure(msg)); }
|
|
||||||
case Success(var val) -> name = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
Article article;
|
|
||||||
switch (Article.create(name, articleNumber, ProductCategoryId.of(cmd.categoryId()), salesUnit)) {
|
|
||||||
case Failure(var err) -> { return Result.failure(err); }
|
|
||||||
case Success(var val) -> article = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (articleRepository.save(article)) {
|
switch (articleRepository.save(article)) {
|
||||||
case Failure(var err) ->
|
case Result.Failure(var err) ->
|
||||||
{ return Result.failure(new ArticleError.RepositoryFailure(err.message())); }
|
{ return Result.failure(new ArticleError.RepositoryFailure(err.message())); }
|
||||||
case Success(var ignored) -> { }
|
case Result.Success(var ignored) -> { }
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.success(article);
|
return Result.success(article);
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@ package de.effigenix.application.masterdata;
|
||||||
|
|
||||||
import de.effigenix.application.masterdata.command.CreateCustomerCommand;
|
import de.effigenix.application.masterdata.command.CreateCustomerCommand;
|
||||||
import de.effigenix.domain.masterdata.*;
|
import de.effigenix.domain.masterdata.*;
|
||||||
import de.effigenix.shared.common.*;
|
import de.effigenix.shared.common.Result;
|
||||||
import de.effigenix.shared.security.ActorId;
|
import de.effigenix.shared.security.ActorId;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import static de.effigenix.shared.common.Result.*;
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public class CreateCustomer {
|
public class CreateCustomer {
|
||||||
|
|
||||||
|
|
@ -18,46 +16,32 @@ public class CreateCustomer {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result<CustomerError, Customer> execute(CreateCustomerCommand cmd, ActorId performedBy) {
|
public Result<CustomerError, Customer> execute(CreateCustomerCommand cmd, ActorId performedBy) {
|
||||||
CustomerName name;
|
var draft = new CustomerDraft(
|
||||||
switch (CustomerName.of(cmd.name())) {
|
cmd.name(), cmd.type(), cmd.phone(), cmd.email(), cmd.contactPerson(),
|
||||||
case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
|
cmd.street(), cmd.houseNumber(), cmd.postalCode(), cmd.city(), cmd.country(),
|
||||||
case Success(var val) -> name = val;
|
cmd.paymentDueDays(), cmd.paymentDescription()
|
||||||
|
);
|
||||||
|
|
||||||
|
Customer customer;
|
||||||
|
switch (Customer.create(draft)) {
|
||||||
|
case Result.Failure(var err) -> { return Result.failure(err); }
|
||||||
|
case Result.Success(var val) -> customer = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (customerRepository.existsByName(name)) {
|
switch (customerRepository.existsByName(customer.name())) {
|
||||||
case Failure(var err) ->
|
case Result.Failure(var err) ->
|
||||||
{ return Result.failure(new CustomerError.RepositoryFailure(err.message())); }
|
{ return Result.failure(new CustomerError.RepositoryFailure(err.message())); }
|
||||||
case Success(var exists) -> {
|
case Result.Success(var exists) -> {
|
||||||
if (exists) {
|
if (exists) {
|
||||||
return Result.failure(new CustomerError.CustomerNameAlreadyExists(cmd.name()));
|
return Result.failure(new CustomerError.CustomerNameAlreadyExists(cmd.name()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Address address;
|
|
||||||
switch (Address.create(cmd.street(), cmd.houseNumber(), cmd.postalCode(), cmd.city(), cmd.country())) {
|
|
||||||
case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
|
|
||||||
case Success(var val) -> address = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
ContactInfo contactInfo;
|
|
||||||
switch (ContactInfo.create(cmd.phone(), cmd.email(), cmd.contactPerson())) {
|
|
||||||
case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
|
|
||||||
case Success(var val) -> contactInfo = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
PaymentTerms paymentTerms;
|
|
||||||
switch (PaymentTerms.create(cmd.paymentDueDays(), cmd.paymentDescription())) {
|
|
||||||
case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
|
|
||||||
case Success(var val) -> paymentTerms = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
var customer = Customer.create(name, cmd.type(), address, contactInfo, paymentTerms);
|
|
||||||
|
|
||||||
switch (customerRepository.save(customer)) {
|
switch (customerRepository.save(customer)) {
|
||||||
case Failure(var err) ->
|
case Result.Failure(var err) ->
|
||||||
{ return Result.failure(new CustomerError.RepositoryFailure(err.message())); }
|
{ return Result.failure(new CustomerError.RepositoryFailure(err.message())); }
|
||||||
case Success(var ignored) -> { }
|
case Result.Success(var ignored) -> { }
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.success(customer);
|
return Result.success(customer);
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@ import de.effigenix.shared.common.Result;
|
||||||
import de.effigenix.shared.security.ActorId;
|
import de.effigenix.shared.security.ActorId;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import static de.effigenix.shared.common.Result.*;
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public class CreateProductCategory {
|
public class CreateProductCategory {
|
||||||
|
|
||||||
|
|
@ -18,28 +16,28 @@ public class CreateProductCategory {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result<ProductCategoryError, ProductCategory> execute(CreateProductCategoryCommand cmd, ActorId performedBy) {
|
public Result<ProductCategoryError, ProductCategory> execute(CreateProductCategoryCommand cmd, ActorId performedBy) {
|
||||||
CategoryName name;
|
var draft = new ProductCategoryDraft(cmd.name(), cmd.description());
|
||||||
switch (CategoryName.of(cmd.name())) {
|
|
||||||
case Failure(var msg) -> { return Result.failure(new ProductCategoryError.ValidationFailure(msg)); }
|
ProductCategory category;
|
||||||
case Success(var val) -> name = val;
|
switch (ProductCategory.create(draft)) {
|
||||||
|
case Result.Failure(var err) -> { return Result.failure(err); }
|
||||||
|
case Result.Success(var val) -> category = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (categoryRepository.existsByName(name)) {
|
switch (categoryRepository.existsByName(category.name())) {
|
||||||
case Failure(var err) ->
|
case Result.Failure(var err) ->
|
||||||
{ return Result.failure(new ProductCategoryError.RepositoryFailure(err.message())); }
|
{ return Result.failure(new ProductCategoryError.RepositoryFailure(err.message())); }
|
||||||
case Success(var exists) -> {
|
case Result.Success(var exists) -> {
|
||||||
if (exists) {
|
if (exists) {
|
||||||
return Result.failure(new ProductCategoryError.CategoryNameAlreadyExists(cmd.name()));
|
return Result.failure(new ProductCategoryError.CategoryNameAlreadyExists(cmd.name()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var category = ProductCategory.create(name, cmd.description());
|
|
||||||
|
|
||||||
switch (categoryRepository.save(category)) {
|
switch (categoryRepository.save(category)) {
|
||||||
case Failure(var err) ->
|
case Result.Failure(var err) ->
|
||||||
{ return Result.failure(new ProductCategoryError.RepositoryFailure(err.message())); }
|
{ return Result.failure(new ProductCategoryError.RepositoryFailure(err.message())); }
|
||||||
case Success(var ignored) -> { }
|
case Result.Success(var ignored) -> { }
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.success(category);
|
return Result.success(category);
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@ package de.effigenix.application.masterdata;
|
||||||
|
|
||||||
import de.effigenix.application.masterdata.command.CreateSupplierCommand;
|
import de.effigenix.application.masterdata.command.CreateSupplierCommand;
|
||||||
import de.effigenix.domain.masterdata.*;
|
import de.effigenix.domain.masterdata.*;
|
||||||
import de.effigenix.shared.common.*;
|
import de.effigenix.shared.common.Result;
|
||||||
import de.effigenix.shared.security.ActorId;
|
import de.effigenix.shared.security.ActorId;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import static de.effigenix.shared.common.Result.*;
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public class CreateSupplier {
|
public class CreateSupplier {
|
||||||
|
|
||||||
|
|
@ -18,46 +16,36 @@ public class CreateSupplier {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result<SupplierError, Supplier> execute(CreateSupplierCommand cmd, ActorId performedBy) {
|
public Result<SupplierError, Supplier> execute(CreateSupplierCommand cmd, ActorId performedBy) {
|
||||||
SupplierName name;
|
// 1. Draft aus Command bauen (kein VO-Wissen im Use Case)
|
||||||
switch (SupplierName.of(cmd.name())) {
|
var draft = new SupplierDraft(
|
||||||
case Failure(var msg) -> { return Result.failure(new SupplierError.ValidationFailure(msg)); }
|
cmd.name(), cmd.phone(), cmd.email(), cmd.contactPerson(),
|
||||||
case Success(var val) -> name = val;
|
cmd.street(), cmd.houseNumber(), cmd.postalCode(), cmd.city(), cmd.country(),
|
||||||
|
cmd.paymentDueDays(), cmd.paymentDescription()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Aggregate erzeugen (validiert intern)
|
||||||
|
Supplier supplier;
|
||||||
|
switch (Supplier.create(draft)) {
|
||||||
|
case Result.Failure(var err) -> { return Result.failure(err); }
|
||||||
|
case Result.Success(var val) -> supplier = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (supplierRepository.existsByName(name)) {
|
// 3. Uniqueness-Check (Application-Concern: braucht Repository)
|
||||||
case Failure(var err) ->
|
switch (supplierRepository.existsByName(supplier.name())) {
|
||||||
|
case Result.Failure(var err) ->
|
||||||
{ return Result.failure(new SupplierError.RepositoryFailure(err.message())); }
|
{ return Result.failure(new SupplierError.RepositoryFailure(err.message())); }
|
||||||
case Success(var exists) -> {
|
case Result.Success(var exists) -> {
|
||||||
if (exists) {
|
if (exists) {
|
||||||
return Result.failure(new SupplierError.SupplierNameAlreadyExists(cmd.name()));
|
return Result.failure(new SupplierError.SupplierNameAlreadyExists(cmd.name()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Address address;
|
// 4. Speichern
|
||||||
switch (Address.create(cmd.street(), cmd.houseNumber(), cmd.postalCode(), cmd.city(), cmd.country())) {
|
|
||||||
case Failure(var msg) -> { return Result.failure(new SupplierError.ValidationFailure(msg)); }
|
|
||||||
case Success(var val) -> address = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
ContactInfo contactInfo;
|
|
||||||
switch (ContactInfo.create(cmd.phone(), cmd.email(), cmd.contactPerson())) {
|
|
||||||
case Failure(var msg) -> { return Result.failure(new SupplierError.ValidationFailure(msg)); }
|
|
||||||
case Success(var val) -> contactInfo = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
PaymentTerms paymentTerms;
|
|
||||||
switch (PaymentTerms.create(cmd.paymentDueDays(), cmd.paymentDescription())) {
|
|
||||||
case Failure(var msg) -> { return Result.failure(new SupplierError.ValidationFailure(msg)); }
|
|
||||||
case Success(var val) -> paymentTerms = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
var supplier = Supplier.create(name, address, contactInfo, paymentTerms);
|
|
||||||
|
|
||||||
switch (supplierRepository.save(supplier)) {
|
switch (supplierRepository.save(supplier)) {
|
||||||
case Failure(var err) ->
|
case Result.Failure(var err) ->
|
||||||
{ return Result.failure(new SupplierError.RepositoryFailure(err.message())); }
|
{ return Result.failure(new SupplierError.RepositoryFailure(err.message())); }
|
||||||
case Success(var ignored) -> { }
|
case Result.Success(var ignored) -> { }
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.success(supplier);
|
return Result.success(supplier);
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@ import de.effigenix.shared.common.Result;
|
||||||
import de.effigenix.shared.security.ActorId;
|
import de.effigenix.shared.security.ActorId;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import static de.effigenix.shared.common.Result.*;
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public class UpdateArticle {
|
public class UpdateArticle {
|
||||||
|
|
||||||
|
|
@ -22,9 +20,9 @@ public class UpdateArticle {
|
||||||
|
|
||||||
Article article;
|
Article article;
|
||||||
switch (articleRepository.findById(articleId)) {
|
switch (articleRepository.findById(articleId)) {
|
||||||
case Failure(var err) ->
|
case Result.Failure(var err) ->
|
||||||
{ return Result.failure(new ArticleError.RepositoryFailure(err.message())); }
|
{ return Result.failure(new ArticleError.RepositoryFailure(err.message())); }
|
||||||
case Success(var opt) -> {
|
case Result.Success(var opt) -> {
|
||||||
if (opt.isEmpty()) {
|
if (opt.isEmpty()) {
|
||||||
return Result.failure(new ArticleError.ArticleNotFound(articleId));
|
return Result.failure(new ArticleError.ArticleNotFound(articleId));
|
||||||
}
|
}
|
||||||
|
|
@ -32,22 +30,16 @@ public class UpdateArticle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd.name() != null) {
|
var draft = new ArticleUpdateDraft(cmd.name(), cmd.categoryId());
|
||||||
ArticleName name;
|
switch (article.update(draft)) {
|
||||||
switch (ArticleName.of(cmd.name())) {
|
case Result.Failure(var err) -> { return Result.failure(err); }
|
||||||
case Failure(var msg) -> { return Result.failure(new ArticleError.ValidationFailure(msg)); }
|
case Result.Success(var ignored) -> { }
|
||||||
case Success(var val) -> name = val;
|
|
||||||
}
|
|
||||||
article.rename(name);
|
|
||||||
}
|
|
||||||
if (cmd.categoryId() != null) {
|
|
||||||
article.changeCategory(ProductCategoryId.of(cmd.categoryId()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (articleRepository.save(article)) {
|
switch (articleRepository.save(article)) {
|
||||||
case Failure(var err) ->
|
case Result.Failure(var err) ->
|
||||||
{ return Result.failure(new ArticleError.RepositoryFailure(err.message())); }
|
{ return Result.failure(new ArticleError.RepositoryFailure(err.message())); }
|
||||||
case Success(var ignored) -> { }
|
case Result.Success(var ignored) -> { }
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.success(article);
|
return Result.success(article);
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@ package de.effigenix.application.masterdata;
|
||||||
|
|
||||||
import de.effigenix.application.masterdata.command.UpdateCustomerCommand;
|
import de.effigenix.application.masterdata.command.UpdateCustomerCommand;
|
||||||
import de.effigenix.domain.masterdata.*;
|
import de.effigenix.domain.masterdata.*;
|
||||||
import de.effigenix.shared.common.*;
|
import de.effigenix.shared.common.Result;
|
||||||
import de.effigenix.shared.security.ActorId;
|
import de.effigenix.shared.security.ActorId;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import static de.effigenix.shared.common.Result.*;
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public class UpdateCustomer {
|
public class UpdateCustomer {
|
||||||
|
|
||||||
|
|
@ -22,9 +20,9 @@ public class UpdateCustomer {
|
||||||
|
|
||||||
Customer customer;
|
Customer customer;
|
||||||
switch (customerRepository.findById(customerId)) {
|
switch (customerRepository.findById(customerId)) {
|
||||||
case Failure(var err) ->
|
case Result.Failure(var err) ->
|
||||||
{ return Result.failure(new CustomerError.RepositoryFailure(err.message())); }
|
{ return Result.failure(new CustomerError.RepositoryFailure(err.message())); }
|
||||||
case Success(var opt) -> {
|
case Result.Success(var opt) -> {
|
||||||
if (opt.isEmpty()) {
|
if (opt.isEmpty()) {
|
||||||
return Result.failure(new CustomerError.CustomerNotFound(customerId));
|
return Result.failure(new CustomerError.CustomerNotFound(customerId));
|
||||||
}
|
}
|
||||||
|
|
@ -32,40 +30,20 @@ public class UpdateCustomer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd.name() != null) {
|
var draft = new CustomerUpdateDraft(
|
||||||
CustomerName name;
|
cmd.name(), cmd.phone(), cmd.email(), cmd.contactPerson(),
|
||||||
switch (CustomerName.of(cmd.name())) {
|
cmd.street(), cmd.houseNumber(), cmd.postalCode(), cmd.city(), cmd.country(),
|
||||||
case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
|
cmd.paymentDueDays(), cmd.paymentDescription()
|
||||||
case Success(var val) -> name = val;
|
);
|
||||||
}
|
switch (customer.update(draft)) {
|
||||||
customer.updateName(name);
|
case Result.Failure(var err) -> { return Result.failure(err); }
|
||||||
|
case Result.Success(var ignored) -> { }
|
||||||
}
|
}
|
||||||
|
|
||||||
Address address;
|
|
||||||
switch (Address.create(cmd.street(), cmd.houseNumber(), cmd.postalCode(), cmd.city(), cmd.country())) {
|
|
||||||
case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
|
|
||||||
case Success(var val) -> address = val;
|
|
||||||
}
|
|
||||||
customer.updateBillingAddress(address);
|
|
||||||
|
|
||||||
ContactInfo contactInfo;
|
|
||||||
switch (ContactInfo.create(cmd.phone(), cmd.email(), cmd.contactPerson())) {
|
|
||||||
case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
|
|
||||||
case Success(var val) -> contactInfo = val;
|
|
||||||
}
|
|
||||||
customer.updateContactInfo(contactInfo);
|
|
||||||
|
|
||||||
PaymentTerms paymentTerms;
|
|
||||||
switch (PaymentTerms.create(cmd.paymentDueDays(), cmd.paymentDescription())) {
|
|
||||||
case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
|
|
||||||
case Success(var val) -> paymentTerms = val;
|
|
||||||
}
|
|
||||||
customer.updatePaymentTerms(paymentTerms);
|
|
||||||
|
|
||||||
switch (customerRepository.save(customer)) {
|
switch (customerRepository.save(customer)) {
|
||||||
case Failure(var err) ->
|
case Result.Failure(var err) ->
|
||||||
{ return Result.failure(new CustomerError.RepositoryFailure(err.message())); }
|
{ return Result.failure(new CustomerError.RepositoryFailure(err.message())); }
|
||||||
case Success(var ignored) -> { }
|
case Result.Success(var ignored) -> { }
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.success(customer);
|
return Result.success(customer);
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@ import de.effigenix.shared.common.Result;
|
||||||
import de.effigenix.shared.security.ActorId;
|
import de.effigenix.shared.security.ActorId;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import static de.effigenix.shared.common.Result.*;
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public class UpdateProductCategory {
|
public class UpdateProductCategory {
|
||||||
|
|
||||||
|
|
@ -22,9 +20,9 @@ public class UpdateProductCategory {
|
||||||
|
|
||||||
ProductCategory category;
|
ProductCategory category;
|
||||||
switch (categoryRepository.findById(categoryId)) {
|
switch (categoryRepository.findById(categoryId)) {
|
||||||
case Failure(var err) ->
|
case Result.Failure(var err) ->
|
||||||
{ return Result.failure(new ProductCategoryError.RepositoryFailure(err.message())); }
|
{ return Result.failure(new ProductCategoryError.RepositoryFailure(err.message())); }
|
||||||
case Success(var opt) -> {
|
case Result.Success(var opt) -> {
|
||||||
if (opt.isEmpty()) {
|
if (opt.isEmpty()) {
|
||||||
return Result.failure(new ProductCategoryError.CategoryNotFound(categoryId));
|
return Result.failure(new ProductCategoryError.CategoryNotFound(categoryId));
|
||||||
}
|
}
|
||||||
|
|
@ -32,22 +30,16 @@ public class UpdateProductCategory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd.name() != null) {
|
var draft = new ProductCategoryUpdateDraft(cmd.name(), cmd.description());
|
||||||
CategoryName name;
|
switch (category.update(draft)) {
|
||||||
switch (CategoryName.of(cmd.name())) {
|
case Result.Failure(var err) -> { return Result.failure(err); }
|
||||||
case Failure(var msg) -> { return Result.failure(new ProductCategoryError.ValidationFailure(msg)); }
|
case Result.Success(var ignored) -> { }
|
||||||
case Success(var val) -> name = val;
|
|
||||||
}
|
|
||||||
category.rename(name);
|
|
||||||
}
|
|
||||||
if (cmd.description() != null) {
|
|
||||||
category.updateDescription(cmd.description());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (categoryRepository.save(category)) {
|
switch (categoryRepository.save(category)) {
|
||||||
case Failure(var err) ->
|
case Result.Failure(var err) ->
|
||||||
{ return Result.failure(new ProductCategoryError.RepositoryFailure(err.message())); }
|
{ return Result.failure(new ProductCategoryError.RepositoryFailure(err.message())); }
|
||||||
case Success(var ignored) -> { }
|
case Result.Success(var ignored) -> { }
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.success(category);
|
return Result.success(category);
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@ package de.effigenix.application.masterdata;
|
||||||
|
|
||||||
import de.effigenix.application.masterdata.command.UpdateSupplierCommand;
|
import de.effigenix.application.masterdata.command.UpdateSupplierCommand;
|
||||||
import de.effigenix.domain.masterdata.*;
|
import de.effigenix.domain.masterdata.*;
|
||||||
import de.effigenix.shared.common.*;
|
import de.effigenix.shared.common.Result;
|
||||||
import de.effigenix.shared.security.ActorId;
|
import de.effigenix.shared.security.ActorId;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import static de.effigenix.shared.common.Result.*;
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public class UpdateSupplier {
|
public class UpdateSupplier {
|
||||||
|
|
||||||
|
|
@ -18,13 +16,14 @@ public class UpdateSupplier {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result<SupplierError, Supplier> execute(UpdateSupplierCommand cmd, ActorId performedBy) {
|
public Result<SupplierError, Supplier> execute(UpdateSupplierCommand cmd, ActorId performedBy) {
|
||||||
|
// 1. Laden
|
||||||
var supplierId = SupplierId.of(cmd.supplierId());
|
var supplierId = SupplierId.of(cmd.supplierId());
|
||||||
|
|
||||||
Supplier supplier;
|
Supplier supplier;
|
||||||
switch (supplierRepository.findById(supplierId)) {
|
switch (supplierRepository.findById(supplierId)) {
|
||||||
case Failure(var err) ->
|
case Result.Failure(var err) ->
|
||||||
{ return Result.failure(new SupplierError.RepositoryFailure(err.message())); }
|
{ return Result.failure(new SupplierError.RepositoryFailure(err.message())); }
|
||||||
case Success(var opt) -> {
|
case Result.Success(var opt) -> {
|
||||||
if (opt.isEmpty()) {
|
if (opt.isEmpty()) {
|
||||||
return Result.failure(new SupplierError.SupplierNotFound(supplierId));
|
return Result.failure(new SupplierError.SupplierNotFound(supplierId));
|
||||||
}
|
}
|
||||||
|
|
@ -32,40 +31,22 @@ public class UpdateSupplier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd.name() != null) {
|
// 2. Draft bauen + Aggregate delegiert Validierung
|
||||||
SupplierName name;
|
var draft = new SupplierUpdateDraft(
|
||||||
switch (SupplierName.of(cmd.name())) {
|
cmd.name(), cmd.phone(), cmd.email(), cmd.contactPerson(),
|
||||||
case Failure(var msg) -> { return Result.failure(new SupplierError.ValidationFailure(msg)); }
|
cmd.street(), cmd.houseNumber(), cmd.postalCode(), cmd.city(), cmd.country(),
|
||||||
case Success(var val) -> name = val;
|
cmd.paymentDueDays(), cmd.paymentDescription()
|
||||||
}
|
);
|
||||||
supplier.updateName(name);
|
switch (supplier.update(draft)) {
|
||||||
|
case Result.Failure(var err) -> { return Result.failure(err); }
|
||||||
|
case Result.Success(var ignored) -> { }
|
||||||
}
|
}
|
||||||
|
|
||||||
Address address;
|
// 3. Speichern
|
||||||
switch (Address.create(cmd.street(), cmd.houseNumber(), cmd.postalCode(), cmd.city(), cmd.country())) {
|
|
||||||
case Failure(var msg) -> { return Result.failure(new SupplierError.ValidationFailure(msg)); }
|
|
||||||
case Success(var val) -> address = val;
|
|
||||||
}
|
|
||||||
supplier.updateAddress(address);
|
|
||||||
|
|
||||||
ContactInfo contactInfo;
|
|
||||||
switch (ContactInfo.create(cmd.phone(), cmd.email(), cmd.contactPerson())) {
|
|
||||||
case Failure(var msg) -> { return Result.failure(new SupplierError.ValidationFailure(msg)); }
|
|
||||||
case Success(var val) -> contactInfo = val;
|
|
||||||
}
|
|
||||||
supplier.updateContactInfo(contactInfo);
|
|
||||||
|
|
||||||
PaymentTerms paymentTerms;
|
|
||||||
switch (PaymentTerms.create(cmd.paymentDueDays(), cmd.paymentDescription())) {
|
|
||||||
case Failure(var msg) -> { return Result.failure(new SupplierError.ValidationFailure(msg)); }
|
|
||||||
case Success(var val) -> paymentTerms = val;
|
|
||||||
}
|
|
||||||
supplier.updatePaymentTerms(paymentTerms);
|
|
||||||
|
|
||||||
switch (supplierRepository.save(supplier)) {
|
switch (supplierRepository.save(supplier)) {
|
||||||
case Failure(var err) ->
|
case Result.Failure(var err) ->
|
||||||
{ return Result.failure(new SupplierError.RepositoryFailure(err.message())); }
|
{ return Result.failure(new SupplierError.RepositoryFailure(err.message())); }
|
||||||
case Success(var ignored) -> { }
|
case Result.Success(var ignored) -> { }
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.success(supplier);
|
return Result.success(supplier);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,6 @@ public record CreateCustomerCommand(
|
||||||
String phone,
|
String phone,
|
||||||
String email,
|
String email,
|
||||||
String contactPerson,
|
String contactPerson,
|
||||||
int paymentDueDays,
|
Integer paymentDueDays,
|
||||||
String paymentDescription
|
String paymentDescription
|
||||||
) {}
|
) {}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
package de.effigenix.application.masterdata.command;
|
package de.effigenix.application.masterdata.command;
|
||||||
|
|
||||||
public record CreateSupplierCommand(
|
public record CreateSupplierCommand(
|
||||||
String name,
|
String name, // required
|
||||||
String street,
|
String phone, // required
|
||||||
String houseNumber,
|
String email, // optional
|
||||||
String postalCode,
|
String contactPerson, // optional
|
||||||
String city,
|
String street, // optional
|
||||||
String country,
|
String houseNumber, // optional
|
||||||
String phone,
|
String postalCode, // optional
|
||||||
String email,
|
String city, // optional
|
||||||
String contactPerson,
|
String country, // optional
|
||||||
int paymentDueDays,
|
Integer paymentDueDays, // optional
|
||||||
String paymentDescription
|
String paymentDescription // optional
|
||||||
) {}
|
) {}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,6 @@ public record UpdateCustomerCommand(
|
||||||
String phone,
|
String phone,
|
||||||
String email,
|
String email,
|
||||||
String contactPerson,
|
String contactPerson,
|
||||||
int paymentDueDays,
|
Integer paymentDueDays,
|
||||||
String paymentDescription
|
String paymentDescription
|
||||||
) {}
|
) {}
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,15 @@ package de.effigenix.application.masterdata.command;
|
||||||
|
|
||||||
public record UpdateSupplierCommand(
|
public record UpdateSupplierCommand(
|
||||||
String supplierId,
|
String supplierId,
|
||||||
String name,
|
String name, // null = nicht ändern
|
||||||
String street,
|
String phone, // null = nicht ändern
|
||||||
String houseNumber,
|
String email, // null = nicht ändern
|
||||||
String postalCode,
|
String contactPerson, // null = nicht ändern
|
||||||
String city,
|
String street, // null = Address nicht ändern
|
||||||
String country,
|
String houseNumber, // null = Address nicht ändern
|
||||||
String phone,
|
String postalCode, // null = Address nicht ändern
|
||||||
String email,
|
String city, // null = Address nicht ändern
|
||||||
String contactPerson,
|
String country, // null = Address nicht ändern
|
||||||
int paymentDueDays,
|
Integer paymentDueDays, // null = PaymentTerms nicht ändern
|
||||||
String paymentDescription
|
String paymentDescription // null = PaymentTerms nicht ändern
|
||||||
) {}
|
) {}
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ public class ChangePassword {
|
||||||
|
|
||||||
// 3. Validate new password
|
// 3. Validate new password
|
||||||
if (!passwordHasher.isValidPassword(cmd.newPassword())) {
|
if (!passwordHasher.isValidPassword(cmd.newPassword())) {
|
||||||
return Result.failure(new UserError.InvalidPassword("Password must be at least 8 characters"));
|
return Result.failure(new UserError.InvalidPassword("Password must be at least 8 characters and contain uppercase, lowercase, digit, and special character"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Hash new password
|
// 4. Hash new password
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ public class CreateUser {
|
||||||
public Result<UserError, UserDTO> execute(CreateUserCommand cmd, ActorId performedBy) {
|
public Result<UserError, UserDTO> execute(CreateUserCommand cmd, ActorId performedBy) {
|
||||||
// 1. Validate password
|
// 1. Validate password
|
||||||
if (!passwordHasher.isValidPassword(cmd.password())) {
|
if (!passwordHasher.isValidPassword(cmd.password())) {
|
||||||
return Result.failure(new UserError.InvalidPassword("Password must be at least 8 characters"));
|
return Result.failure(new UserError.InvalidPassword("Password must be at least 8 characters and contain uppercase, lowercase, digit, and special character"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check username uniqueness
|
// 2. Check username uniqueness
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package de.effigenix.domain.masterdata;
|
||||||
import de.effigenix.shared.common.Money;
|
import de.effigenix.shared.common.Money;
|
||||||
import de.effigenix.shared.common.Result;
|
import de.effigenix.shared.common.Result;
|
||||||
|
|
||||||
|
import static de.effigenix.shared.common.Result.*;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
|
@ -49,22 +51,50 @@ public class Article {
|
||||||
this.updatedAt = updatedAt;
|
this.updatedAt = updatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Result<ArticleError, Article> create(
|
/**
|
||||||
ArticleName name,
|
* Factory: Erzeugt einen neuen Article aus rohen Eingaben.
|
||||||
ArticleNumber articleNumber,
|
* Orchestriert Validierung aller VOs intern.
|
||||||
ProductCategoryId categoryId,
|
* Alle Felder sind Pflicht.
|
||||||
SalesUnit initialSalesUnit
|
*/
|
||||||
) {
|
public static Result<ArticleError, Article> create(ArticleDraft draft) {
|
||||||
if (initialSalesUnit == null) {
|
// 1. ArticleName validieren
|
||||||
return Result.failure(new ArticleError.MinimumSalesUnitRequired());
|
ArticleName name;
|
||||||
|
switch (ArticleName.of(draft.name())) {
|
||||||
|
case Failure(var msg) -> { return Result.failure(new ArticleError.ValidationFailure(msg)); }
|
||||||
|
case Success(var val) -> name = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. ArticleNumber validieren
|
||||||
|
ArticleNumber articleNumber;
|
||||||
|
switch (ArticleNumber.of(draft.articleNumber())) {
|
||||||
|
case Failure(var msg) -> { return Result.failure(new ArticleError.ValidationFailure(msg)); }
|
||||||
|
case Success(var val) -> articleNumber = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. ProductCategoryId – reines UUID-Wrapping
|
||||||
|
var categoryId = ProductCategoryId.of(draft.categoryId());
|
||||||
|
|
||||||
|
// 4. Money validieren
|
||||||
|
Money price;
|
||||||
|
switch (Money.tryEuro(draft.price())) {
|
||||||
|
case Failure(var msg) -> { return Result.failure(new ArticleError.ValidationFailure(msg)); }
|
||||||
|
case Success(var val) -> price = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. SalesUnit erzeugen (initiale Pflicht-Einheit)
|
||||||
|
SalesUnit salesUnit;
|
||||||
|
switch (SalesUnit.create(draft.unit(), draft.priceModel(), price)) {
|
||||||
|
case Failure(var err) -> { return Result.failure(err); }
|
||||||
|
case Success(var val) -> salesUnit = val;
|
||||||
|
}
|
||||||
|
|
||||||
var now = LocalDateTime.now();
|
var now = LocalDateTime.now();
|
||||||
return Result.success(new Article(
|
return Result.success(new Article(
|
||||||
ArticleId.generate(),
|
ArticleId.generate(),
|
||||||
name,
|
name,
|
||||||
articleNumber,
|
articleNumber,
|
||||||
categoryId,
|
categoryId,
|
||||||
List.of(initialSalesUnit),
|
List.of(salesUnit),
|
||||||
ArticleStatus.ACTIVE,
|
ArticleStatus.ACTIVE,
|
||||||
Set.of(),
|
Set.of(),
|
||||||
now,
|
now,
|
||||||
|
|
@ -124,14 +154,22 @@ public class Article {
|
||||||
|
|
||||||
// ==================== Article Properties ====================
|
// ==================== Article Properties ====================
|
||||||
|
|
||||||
public void rename(ArticleName newName) {
|
/**
|
||||||
this.name = newName;
|
* Wendet partielle Updates an. Das Aggregate validiert intern.
|
||||||
touch();
|
* null-Felder im Draft werden ignoriert.
|
||||||
}
|
*/
|
||||||
|
public Result<ArticleError, Void> update(ArticleUpdateDraft draft) {
|
||||||
public void changeCategory(ProductCategoryId newCategoryId) {
|
if (draft.name() != null) {
|
||||||
this.categoryId = newCategoryId;
|
switch (ArticleName.of(draft.name())) {
|
||||||
|
case Failure(var msg) -> { return Result.failure(new ArticleError.ValidationFailure(msg)); }
|
||||||
|
case Success(var val) -> this.name = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (draft.categoryId() != null) {
|
||||||
|
this.categoryId = ProductCategoryId.of(draft.categoryId());
|
||||||
|
}
|
||||||
touch();
|
touch();
|
||||||
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void activate() {
|
public void activate() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package de.effigenix.domain.masterdata;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rohe Eingabe zum Erzeugen eines Article.
|
||||||
|
* Wird vom Application Layer aus dem Command gebaut und an Article.create() übergeben.
|
||||||
|
* Das Aggregate ist verantwortlich für Validierung und VO-Konstruktion.
|
||||||
|
*
|
||||||
|
* @param name Pflicht
|
||||||
|
* @param articleNumber Pflicht
|
||||||
|
* @param categoryId Pflicht (UUID-String)
|
||||||
|
* @param unit Pflicht (Enum)
|
||||||
|
* @param priceModel Pflicht (Enum)
|
||||||
|
* @param price Pflicht (Article muss mind. eine SalesUnit haben)
|
||||||
|
*/
|
||||||
|
public record ArticleDraft(
|
||||||
|
String name,
|
||||||
|
String articleNumber,
|
||||||
|
String categoryId,
|
||||||
|
Unit unit,
|
||||||
|
PriceModel priceModel,
|
||||||
|
BigDecimal price
|
||||||
|
) {}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package de.effigenix.domain.masterdata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rohe Eingabe für partielle Article-Updates.
|
||||||
|
* null-Felder bedeuten "nicht ändern".
|
||||||
|
* Wird vom Application Layer aus dem Command gebaut und an Article.update() übergeben.
|
||||||
|
*
|
||||||
|
* @param name null = nicht ändern
|
||||||
|
* @param categoryId null = nicht ändern
|
||||||
|
*/
|
||||||
|
public record ArticleUpdateDraft(
|
||||||
|
String name,
|
||||||
|
String categoryId
|
||||||
|
) {}
|
||||||
|
|
@ -6,6 +6,8 @@ import de.effigenix.shared.common.Money;
|
||||||
import de.effigenix.shared.common.PaymentTerms;
|
import de.effigenix.shared.common.PaymentTerms;
|
||||||
import de.effigenix.shared.common.Result;
|
import de.effigenix.shared.common.Result;
|
||||||
|
|
||||||
|
import static de.effigenix.shared.common.Result.*;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
|
@ -13,9 +15,11 @@ import java.util.*;
|
||||||
* Aggregate Root for Customer.
|
* Aggregate Root for Customer.
|
||||||
*
|
*
|
||||||
* Invariants:
|
* Invariants:
|
||||||
* 1. Name, CustomerType, billingAddress non-null
|
* 1. Name, CustomerType und billingAddress sind Pflicht
|
||||||
* 2. FrameContract only for B2B customers
|
* 2. ContactInfo (phone) ist Pflicht
|
||||||
* 3. No duplicate ContractLineItems per ArticleId (enforced by FrameContract)
|
* 3. PaymentTerms ist optional
|
||||||
|
* 4. FrameContract nur für B2B-Kunden
|
||||||
|
* 5. No duplicate ContractLineItems per ArticleId (enforced by FrameContract)
|
||||||
*/
|
*/
|
||||||
public class Customer {
|
public class Customer {
|
||||||
|
|
||||||
|
|
@ -60,18 +64,47 @@ public class Customer {
|
||||||
this.updatedAt = updatedAt;
|
this.updatedAt = updatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Customer create(
|
/**
|
||||||
CustomerName name,
|
* Factory: Erzeugt einen neuen Customer aus rohen Eingaben.
|
||||||
CustomerType type,
|
* Orchestriert Validierung aller VOs intern.
|
||||||
Address billingAddress,
|
* Pflicht: name, type, phone, street, city, country. Optional: alles andere.
|
||||||
ContactInfo contactInfo,
|
*/
|
||||||
PaymentTerms paymentTerms
|
public static Result<CustomerError, Customer> create(CustomerDraft draft) {
|
||||||
) {
|
// 1. Name validieren (Pflicht)
|
||||||
|
CustomerName name;
|
||||||
|
switch (CustomerName.of(draft.name())) {
|
||||||
|
case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
|
||||||
|
case Success(var val) -> name = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. ContactInfo validieren (Pflicht: phone; optional: email, contactPerson)
|
||||||
|
ContactInfo contactInfo;
|
||||||
|
switch (ContactInfo.create(draft.phone(), draft.email(), draft.contactPerson())) {
|
||||||
|
case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
|
||||||
|
case Success(var val) -> contactInfo = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. billingAddress (Pflicht-Invariante)
|
||||||
|
Address billingAddress;
|
||||||
|
switch (Address.create(draft.street(), draft.houseNumber(), draft.postalCode(), draft.city(), draft.country())) {
|
||||||
|
case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
|
||||||
|
case Success(var val) -> billingAddress = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. PaymentTerms optional
|
||||||
|
PaymentTerms paymentTerms = null;
|
||||||
|
if (draft.paymentDueDays() != null) {
|
||||||
|
switch (PaymentTerms.create(draft.paymentDueDays(), draft.paymentDescription())) {
|
||||||
|
case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
|
||||||
|
case Success(var val) -> paymentTerms = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var now = LocalDateTime.now();
|
var now = LocalDateTime.now();
|
||||||
return new Customer(
|
return Result.success(new Customer(
|
||||||
CustomerId.generate(), name, type, billingAddress, contactInfo, paymentTerms,
|
CustomerId.generate(), name, draft.type(), billingAddress, contactInfo, paymentTerms,
|
||||||
List.of(), null, Set.of(), CustomerStatus.ACTIVE, now, now
|
List.of(), null, Set.of(), CustomerStatus.ACTIVE, now, now
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Customer reconstitute(
|
public static Customer reconstitute(
|
||||||
|
|
@ -94,24 +127,41 @@ public class Customer {
|
||||||
|
|
||||||
// ==================== Business Methods ====================
|
// ==================== Business Methods ====================
|
||||||
|
|
||||||
public void updateName(CustomerName newName) {
|
/**
|
||||||
this.name = newName;
|
* Wendet partielle Updates an. Das Aggregate validiert intern.
|
||||||
touch();
|
* null-Felder im Draft werden ignoriert.
|
||||||
}
|
*/
|
||||||
|
public Result<CustomerError, Void> update(CustomerUpdateDraft draft) {
|
||||||
|
if (draft.name() != null) {
|
||||||
|
switch (CustomerName.of(draft.name())) {
|
||||||
|
case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
|
||||||
|
case Success(var val) -> this.name = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void updateBillingAddress(Address newAddress) {
|
if (draft.phone() != null) {
|
||||||
this.billingAddress = newAddress;
|
switch (ContactInfo.create(draft.phone(), draft.email(), draft.contactPerson())) {
|
||||||
touch();
|
case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
|
||||||
}
|
case Success(var val) -> this.contactInfo = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void updateContactInfo(ContactInfo newContactInfo) {
|
if (draft.street() != null || draft.city() != null) {
|
||||||
this.contactInfo = newContactInfo;
|
switch (Address.create(draft.street(), draft.houseNumber(), draft.postalCode(), draft.city(), draft.country())) {
|
||||||
touch();
|
case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
|
||||||
}
|
case Success(var val) -> this.billingAddress = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draft.paymentDueDays() != null) {
|
||||||
|
switch (PaymentTerms.create(draft.paymentDueDays(), draft.paymentDescription())) {
|
||||||
|
case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
|
||||||
|
case Success(var val) -> this.paymentTerms = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void updatePaymentTerms(PaymentTerms newPaymentTerms) {
|
|
||||||
this.paymentTerms = newPaymentTerms;
|
|
||||||
touch();
|
touch();
|
||||||
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Delivery Addresses ====================
|
// ==================== Delivery Addresses ====================
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package de.effigenix.domain.masterdata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rohe Eingabe zum Erzeugen eines Customer.
|
||||||
|
* Wird vom Application Layer aus dem Command gebaut und an Customer.create() übergeben.
|
||||||
|
* Das Aggregate ist verantwortlich für Validierung und VO-Konstruktion.
|
||||||
|
*
|
||||||
|
* @param name Pflicht
|
||||||
|
* @param type Pflicht (Enum)
|
||||||
|
* @param phone Pflicht (Teil von ContactInfo)
|
||||||
|
* @param email Optional
|
||||||
|
* @param contactPerson Optional
|
||||||
|
* @param street Pflicht (billingAddress ist Pflicht-Invariante)
|
||||||
|
* @param houseNumber Optional
|
||||||
|
* @param postalCode Optional
|
||||||
|
* @param city Pflicht
|
||||||
|
* @param country Pflicht (ISO 3166-1 alpha-2)
|
||||||
|
* @param paymentDueDays Optional
|
||||||
|
* @param paymentDescription Optional
|
||||||
|
*/
|
||||||
|
public record CustomerDraft(
|
||||||
|
String name,
|
||||||
|
CustomerType type,
|
||||||
|
String phone,
|
||||||
|
String email,
|
||||||
|
String contactPerson,
|
||||||
|
String street,
|
||||||
|
String houseNumber,
|
||||||
|
String postalCode,
|
||||||
|
String city,
|
||||||
|
String country,
|
||||||
|
Integer paymentDueDays,
|
||||||
|
String paymentDescription
|
||||||
|
) {}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package de.effigenix.domain.masterdata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rohe Eingabe für partielle Customer-Updates.
|
||||||
|
* null-Felder bedeuten "nicht ändern".
|
||||||
|
* Wird vom Application Layer aus dem Command gebaut und an Customer.update() übergeben.
|
||||||
|
*
|
||||||
|
* @param name null = nicht ändern
|
||||||
|
* @param phone null = ContactInfo nicht ändern
|
||||||
|
* @param email Teil von ContactInfo (nur relevant wenn phone != null)
|
||||||
|
* @param contactPerson Teil von ContactInfo (nur relevant wenn phone != null)
|
||||||
|
* @param street null = billingAddress nicht ändern
|
||||||
|
* @param houseNumber Teil von billingAddress
|
||||||
|
* @param postalCode Teil von billingAddress
|
||||||
|
* @param city Teil von billingAddress
|
||||||
|
* @param country Teil von billingAddress
|
||||||
|
* @param paymentDueDays null = PaymentTerms nicht ändern
|
||||||
|
* @param paymentDescription Teil von PaymentTerms
|
||||||
|
*/
|
||||||
|
public record CustomerUpdateDraft(
|
||||||
|
String name,
|
||||||
|
String phone,
|
||||||
|
String email,
|
||||||
|
String contactPerson,
|
||||||
|
String street,
|
||||||
|
String houseNumber,
|
||||||
|
String postalCode,
|
||||||
|
String city,
|
||||||
|
String country,
|
||||||
|
Integer paymentDueDays,
|
||||||
|
String paymentDescription
|
||||||
|
) {}
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
package de.effigenix.domain.masterdata;
|
package de.effigenix.domain.masterdata;
|
||||||
|
|
||||||
|
import de.effigenix.shared.common.Result;
|
||||||
|
|
||||||
|
import static de.effigenix.shared.common.Result.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mini-Aggregate for product categories.
|
* Mini-Aggregate for product categories.
|
||||||
* DB-based entity, dynamically extendable by users.
|
* DB-based entity, dynamically extendable by users.
|
||||||
|
|
@ -16,20 +20,38 @@ public class ProductCategory {
|
||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ProductCategory create(CategoryName name, String description) {
|
/**
|
||||||
return new ProductCategory(ProductCategoryId.generate(), name, description);
|
* Factory: Erzeugt eine neue ProductCategory aus rohen Eingaben.
|
||||||
|
* Orchestriert Validierung des CategoryName-VO intern.
|
||||||
|
*/
|
||||||
|
public static Result<ProductCategoryError, ProductCategory> create(ProductCategoryDraft draft) {
|
||||||
|
CategoryName name;
|
||||||
|
switch (CategoryName.of(draft.name())) {
|
||||||
|
case Failure(var msg) -> { return Result.failure(new ProductCategoryError.ValidationFailure(msg)); }
|
||||||
|
case Success(var val) -> name = val;
|
||||||
|
}
|
||||||
|
return Result.success(new ProductCategory(ProductCategoryId.generate(), name, draft.description()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ProductCategory reconstitute(ProductCategoryId id, CategoryName name, String description) {
|
public static ProductCategory reconstitute(ProductCategoryId id, CategoryName name, String description) {
|
||||||
return new ProductCategory(id, name, description);
|
return new ProductCategory(id, name, description);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void rename(CategoryName newName) {
|
/**
|
||||||
this.name = newName;
|
* Wendet partielle Updates an. Das Aggregate validiert intern.
|
||||||
}
|
* null-Felder im Draft werden ignoriert.
|
||||||
|
*/
|
||||||
public void updateDescription(String newDescription) {
|
public Result<ProductCategoryError, Void> update(ProductCategoryUpdateDraft draft) {
|
||||||
this.description = newDescription;
|
if (draft.name() != null) {
|
||||||
|
switch (CategoryName.of(draft.name())) {
|
||||||
|
case Failure(var msg) -> { return Result.failure(new ProductCategoryError.ValidationFailure(msg)); }
|
||||||
|
case Success(var val) -> this.name = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (draft.description() != null) {
|
||||||
|
this.description = draft.description();
|
||||||
|
}
|
||||||
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProductCategoryId id() { return id; }
|
public ProductCategoryId id() { return id; }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package de.effigenix.domain.masterdata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rohe Eingabe zum Erzeugen einer ProductCategory.
|
||||||
|
* Wird vom Application Layer aus dem Command gebaut und an ProductCategory.create() übergeben.
|
||||||
|
* Das Aggregate ist verantwortlich für Validierung und VO-Konstruktion.
|
||||||
|
*
|
||||||
|
* @param name Pflicht
|
||||||
|
* @param description Optional
|
||||||
|
*/
|
||||||
|
public record ProductCategoryDraft(
|
||||||
|
String name,
|
||||||
|
String description
|
||||||
|
) {}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package de.effigenix.domain.masterdata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rohe Eingabe für partielle ProductCategory-Updates.
|
||||||
|
* null-Felder bedeuten "nicht ändern".
|
||||||
|
* Wird vom Application Layer aus dem Command gebaut und an ProductCategory.update() übergeben.
|
||||||
|
*
|
||||||
|
* @param name null = nicht ändern
|
||||||
|
* @param description null = nicht ändern
|
||||||
|
*/
|
||||||
|
public record ProductCategoryUpdateDraft(
|
||||||
|
String name,
|
||||||
|
String description
|
||||||
|
) {}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package de.effigenix.domain.masterdata;
|
package de.effigenix.domain.masterdata;
|
||||||
|
|
||||||
|
import de.effigenix.shared.common.Result;
|
||||||
import de.effigenix.shared.common.Address;
|
import de.effigenix.shared.common.Address;
|
||||||
import de.effigenix.shared.common.ContactInfo;
|
import de.effigenix.shared.common.ContactInfo;
|
||||||
import de.effigenix.shared.common.PaymentTerms;
|
import de.effigenix.shared.common.PaymentTerms;
|
||||||
|
|
@ -9,13 +10,16 @@ import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static de.effigenix.shared.common.Result.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aggregate Root for Supplier.
|
* Aggregate Root for Supplier.
|
||||||
*
|
*
|
||||||
* Invariants:
|
* Invariants:
|
||||||
* 1. Name, Address, PaymentTerms non-null
|
* 1. Name und ContactInfo (phone) sind Pflicht
|
||||||
* 2. No structurally identical certificates (record equality)
|
* 2. Address und PaymentTerms sind optional
|
||||||
* 3. Rating scores 1-5 (enforced by SupplierRating VO)
|
* 3. No structurally identical certificates (record equality)
|
||||||
|
* 4. Rating scores 1-5 (enforced by SupplierRating VO)
|
||||||
*/
|
*/
|
||||||
public class Supplier {
|
public class Supplier {
|
||||||
|
|
||||||
|
|
@ -54,17 +58,49 @@ public class Supplier {
|
||||||
this.updatedAt = updatedAt;
|
this.updatedAt = updatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Supplier create(
|
/**
|
||||||
SupplierName name,
|
* Factory: Erzeugt einen neuen Supplier aus rohen Eingaben.
|
||||||
Address address,
|
* Orchestriert Validierung aller VOs intern.
|
||||||
ContactInfo contactInfo,
|
* Pflicht: name, phone. Optional: alles andere.
|
||||||
PaymentTerms paymentTerms
|
*/
|
||||||
) {
|
public static Result<SupplierError, Supplier> create(SupplierDraft draft) {
|
||||||
|
// 1. Name validieren (Pflicht)
|
||||||
|
SupplierName name;
|
||||||
|
switch (SupplierName.of(draft.name())) {
|
||||||
|
case Failure(var msg) -> { return Result.failure(new SupplierError.ValidationFailure(msg)); }
|
||||||
|
case Success(var val) -> name = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. ContactInfo validieren (Pflicht: phone; optional: email, contactPerson)
|
||||||
|
ContactInfo contactInfo;
|
||||||
|
switch (ContactInfo.create(draft.phone(), draft.email(), draft.contactPerson())) {
|
||||||
|
case Failure(var msg) -> { return Result.failure(new SupplierError.ValidationFailure(msg)); }
|
||||||
|
case Success(var val) -> contactInfo = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Address optional – nur wenn mindestens street oder city gesetzt
|
||||||
|
Address address = null;
|
||||||
|
if (draft.street() != null || draft.city() != null) {
|
||||||
|
switch (Address.create(draft.street(), draft.houseNumber(), draft.postalCode(), draft.city(), draft.country())) {
|
||||||
|
case Failure(var msg) -> { return Result.failure(new SupplierError.ValidationFailure(msg)); }
|
||||||
|
case Success(var val) -> address = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. PaymentTerms optional
|
||||||
|
PaymentTerms paymentTerms = null;
|
||||||
|
if (draft.paymentDueDays() != null) {
|
||||||
|
switch (PaymentTerms.create(draft.paymentDueDays(), draft.paymentDescription())) {
|
||||||
|
case Failure(var msg) -> { return Result.failure(new SupplierError.ValidationFailure(msg)); }
|
||||||
|
case Success(var val) -> paymentTerms = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var now = LocalDateTime.now();
|
var now = LocalDateTime.now();
|
||||||
return new Supplier(
|
return Result.success(new Supplier(
|
||||||
SupplierId.generate(), name, address, contactInfo, paymentTerms,
|
SupplierId.generate(), name, address, contactInfo, paymentTerms,
|
||||||
List.of(), null, SupplierStatus.ACTIVE, now, now
|
List.of(), null, SupplierStatus.ACTIVE, now, now
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Supplier reconstitute(
|
public static Supplier reconstitute(
|
||||||
|
|
@ -85,24 +121,41 @@ public class Supplier {
|
||||||
|
|
||||||
// ==================== Business Methods ====================
|
// ==================== Business Methods ====================
|
||||||
|
|
||||||
public void updateName(SupplierName newName) {
|
/**
|
||||||
this.name = newName;
|
* Wendet partielle Updates an. Das Aggregate validiert intern.
|
||||||
touch();
|
* null-Felder im Draft werden ignoriert.
|
||||||
}
|
*/
|
||||||
|
public Result<SupplierError, Void> update(SupplierUpdateDraft draft) {
|
||||||
|
if (draft.name() != null) {
|
||||||
|
switch (SupplierName.of(draft.name())) {
|
||||||
|
case Failure(var msg) -> { return Result.failure(new SupplierError.ValidationFailure(msg)); }
|
||||||
|
case Success(var val) -> this.name = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void updateAddress(Address newAddress) {
|
if (draft.phone() != null) {
|
||||||
this.address = newAddress;
|
switch (ContactInfo.create(draft.phone(), draft.email(), draft.contactPerson())) {
|
||||||
touch();
|
case Failure(var msg) -> { return Result.failure(new SupplierError.ValidationFailure(msg)); }
|
||||||
}
|
case Success(var val) -> this.contactInfo = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void updateContactInfo(ContactInfo newContactInfo) {
|
if (draft.street() != null || draft.city() != null) {
|
||||||
this.contactInfo = newContactInfo;
|
switch (Address.create(draft.street(), draft.houseNumber(), draft.postalCode(), draft.city(), draft.country())) {
|
||||||
touch();
|
case Failure(var msg) -> { return Result.failure(new SupplierError.ValidationFailure(msg)); }
|
||||||
}
|
case Success(var val) -> this.address = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draft.paymentDueDays() != null) {
|
||||||
|
switch (PaymentTerms.create(draft.paymentDueDays(), draft.paymentDescription())) {
|
||||||
|
case Failure(var msg) -> { return Result.failure(new SupplierError.ValidationFailure(msg)); }
|
||||||
|
case Success(var val) -> this.paymentTerms = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void updatePaymentTerms(PaymentTerms newPaymentTerms) {
|
|
||||||
this.paymentTerms = newPaymentTerms;
|
|
||||||
touch();
|
touch();
|
||||||
|
return Result.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addCertificate(QualityCertificate certificate) {
|
public void addCertificate(QualityCertificate certificate) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package de.effigenix.domain.masterdata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rohe Eingabe zum Erzeugen eines Supplier.
|
||||||
|
* Wird vom Application Layer aus dem Command gebaut und an Supplier.create() übergeben.
|
||||||
|
* Das Aggregate ist verantwortlich für Validierung und VO-Konstruktion.
|
||||||
|
*
|
||||||
|
* @param name Pflicht
|
||||||
|
* @param phone Pflicht (Teil von ContactInfo)
|
||||||
|
* @param email Optional
|
||||||
|
* @param contactPerson Optional
|
||||||
|
* @param street Optional – nur wenn Address insgesamt vollständig
|
||||||
|
* @param houseNumber Optional
|
||||||
|
* @param postalCode Optional
|
||||||
|
* @param city Optional
|
||||||
|
* @param country Optional
|
||||||
|
* @param paymentDueDays Optional
|
||||||
|
* @param paymentDescription Optional
|
||||||
|
*/
|
||||||
|
public record SupplierDraft(
|
||||||
|
String name,
|
||||||
|
String phone,
|
||||||
|
String email,
|
||||||
|
String contactPerson,
|
||||||
|
String street,
|
||||||
|
String houseNumber,
|
||||||
|
String postalCode,
|
||||||
|
String city,
|
||||||
|
String country,
|
||||||
|
Integer paymentDueDays,
|
||||||
|
String paymentDescription
|
||||||
|
) {}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package de.effigenix.domain.masterdata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rohe Eingabe für partielle Supplier-Updates.
|
||||||
|
* null-Felder bedeuten "nicht ändern".
|
||||||
|
*
|
||||||
|
* @param name null = nicht ändern
|
||||||
|
* @param phone null = nicht ändern
|
||||||
|
* @param email null = nicht ändern
|
||||||
|
* @param contactPerson null = nicht ändern
|
||||||
|
* @param street null = Address nicht ändern
|
||||||
|
* @param houseNumber null = Address nicht ändern
|
||||||
|
* @param postalCode null = Address nicht ändern
|
||||||
|
* @param city null = Address nicht ändern
|
||||||
|
* @param country null = Address nicht ändern
|
||||||
|
* @param paymentDueDays null = PaymentTerms nicht ändern
|
||||||
|
* @param paymentDescription null = PaymentTerms nicht ändern
|
||||||
|
*/
|
||||||
|
public record SupplierUpdateDraft(
|
||||||
|
String name,
|
||||||
|
String phone,
|
||||||
|
String email,
|
||||||
|
String contactPerson,
|
||||||
|
String street,
|
||||||
|
String houseNumber,
|
||||||
|
String postalCode,
|
||||||
|
String city,
|
||||||
|
String country,
|
||||||
|
Integer paymentDueDays,
|
||||||
|
String paymentDescription
|
||||||
|
) {}
|
||||||
|
|
@ -6,7 +6,6 @@ import de.effigenix.shared.security.ActorId;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.scheduling.annotation.Async;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Propagation;
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
@ -20,7 +19,6 @@ import java.util.UUID;
|
||||||
* Database-backed implementation of AuditLogger.
|
* Database-backed implementation of AuditLogger.
|
||||||
*
|
*
|
||||||
* HACCP/GoBD Compliance:
|
* HACCP/GoBD Compliance:
|
||||||
* - All operations are async (@Async) for performance (don't block business logic)
|
|
||||||
* - Logs are written to database for durability
|
* - Logs are written to database for durability
|
||||||
* - Logs are immutable after creation
|
* - Logs are immutable after creation
|
||||||
* - Includes IP address and user agent for forensics
|
* - Includes IP address and user agent for forensics
|
||||||
|
|
@ -43,7 +41,6 @@ public class DatabaseAuditLogger implements AuditLogger {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Async
|
|
||||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
public void log(AuditEvent event, String entityId, ActorId performedBy) {
|
public void log(AuditEvent event, String entityId, ActorId performedBy) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -67,7 +64,6 @@ public class DatabaseAuditLogger implements AuditLogger {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Async
|
|
||||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
public void log(AuditEvent event, String details) {
|
public void log(AuditEvent event, String details) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -90,7 +86,6 @@ public class DatabaseAuditLogger implements AuditLogger {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Async
|
|
||||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
public void log(AuditEvent event, ActorId performedBy) {
|
public void log(AuditEvent event, ActorId performedBy) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ VALUES (
|
||||||
'00000000-0000-0000-0000-000000000001', -- Fixed UUID for admin
|
'00000000-0000-0000-0000-000000000001', -- Fixed UUID for admin
|
||||||
'admin',
|
'admin',
|
||||||
'admin@effigenix.com',
|
'admin@effigenix.com',
|
||||||
'$2a$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYKKHFw3zqm', -- BCrypt hash for "admin123"
|
'$2a$12$SJmX80hUZoA66W77CX7cHeRw1TPscXD6S8HYEZfhJ5PxTfkbwbLdi', -- BCrypt hash for "admin123"
|
||||||
NULL, -- No branch = global access
|
NULL, -- No branch = global access
|
||||||
'ACTIVE',
|
'ACTIVE',
|
||||||
CURRENT_TIMESTAMP,
|
CURRENT_TIMESTAMP,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue