mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 06:39:34 +01:00
test: Unit-Tests für Masterdata-Domain und Application Layer
Domain-Tests: Article, Customer, ProductCategory, Supplier Application-Tests: ArticleUseCase, CustomerUseCase, ProductCategoryUseCase, SupplierUseCase, ListStorageLocations JaCoCo: Stub-Paket von Coverage-Analyse ausgeschlossen
This commit is contained in:
parent
d7fcc946e7
commit
8a9bf849a9
10 changed files with 6453 additions and 0 deletions
|
|
@ -150,6 +150,11 @@
|
|||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>${jacoco.version}</version>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>de/effigenix/infrastructure/stub/**</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare-agent</id>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,234 @@
|
|||
package de.effigenix.application.inventory;
|
||||
|
||||
import de.effigenix.domain.inventory.*;
|
||||
import de.effigenix.shared.common.RepositoryError;
|
||||
import de.effigenix.shared.common.Result;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("ListStorageLocations Use Case")
|
||||
class ListStorageLocationsTest {
|
||||
|
||||
@Mock private StorageLocationRepository storageLocationRepository;
|
||||
|
||||
private ListStorageLocations listStorageLocations;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
listStorageLocations = new ListStorageLocations(storageLocationRepository);
|
||||
}
|
||||
|
||||
private StorageLocation activeLocation(String name, StorageType type) {
|
||||
return StorageLocation.reconstitute(
|
||||
StorageLocationId.generate(),
|
||||
new StorageLocationName(name),
|
||||
type, null, true
|
||||
);
|
||||
}
|
||||
|
||||
private StorageLocation inactiveLocation(String name, StorageType type) {
|
||||
return StorageLocation.reconstitute(
|
||||
StorageLocationId.generate(),
|
||||
new StorageLocationName(name),
|
||||
type, null, false
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== Ohne Filter ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("ohne Filter")
|
||||
class NoFilter {
|
||||
|
||||
@Test
|
||||
@DisplayName("should return all storage locations")
|
||||
void shouldReturnAll() {
|
||||
var locations = List.of(
|
||||
activeLocation("Lager A", StorageType.DRY_STORAGE),
|
||||
inactiveLocation("Lager B", StorageType.COLD_ROOM)
|
||||
);
|
||||
when(storageLocationRepository.findAll()).thenReturn(Result.success(locations));
|
||||
|
||||
var result = listStorageLocations.execute(null, null);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return empty list when none exist")
|
||||
void shouldReturnEmptyList() {
|
||||
when(storageLocationRepository.findAll()).thenReturn(Result.success(List.of()));
|
||||
|
||||
var result = listStorageLocations.execute(null, null);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when repository fails")
|
||||
void shouldFailWhenRepositoryFails() {
|
||||
when(storageLocationRepository.findAll())
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = listStorageLocations.execute(null, null);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(StorageLocationError.RepositoryFailure.class);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Active-Filter ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("active-Filter")
|
||||
class ActiveFilter {
|
||||
|
||||
@Test
|
||||
@DisplayName("should return only active locations when active=true")
|
||||
void shouldReturnOnlyActive() {
|
||||
var locations = List.of(
|
||||
activeLocation("Aktiv", StorageType.DRY_STORAGE),
|
||||
inactiveLocation("Inaktiv", StorageType.COLD_ROOM)
|
||||
);
|
||||
when(storageLocationRepository.findAll()).thenReturn(Result.success(locations));
|
||||
|
||||
var result = listStorageLocations.execute(null, true);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).hasSize(1);
|
||||
assertThat(result.unsafeGetValue().get(0).name().value()).isEqualTo("Aktiv");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return only inactive locations when active=false")
|
||||
void shouldReturnOnlyInactive() {
|
||||
var locations = List.of(
|
||||
activeLocation("Aktiv", StorageType.DRY_STORAGE),
|
||||
inactiveLocation("Inaktiv", StorageType.COLD_ROOM)
|
||||
);
|
||||
when(storageLocationRepository.findAll()).thenReturn(Result.success(locations));
|
||||
|
||||
var result = listStorageLocations.execute(null, false);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).hasSize(1);
|
||||
assertThat(result.unsafeGetValue().get(0).name().value()).isEqualTo("Inaktiv");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return empty list when no locations match active filter")
|
||||
void shouldReturnEmptyWhenNoMatch() {
|
||||
var locations = List.of(
|
||||
activeLocation("Aktiv", StorageType.DRY_STORAGE)
|
||||
);
|
||||
when(storageLocationRepository.findAll()).thenReturn(Result.success(locations));
|
||||
|
||||
var result = listStorageLocations.execute(null, false);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== StorageType-Filter ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("storageType-Filter")
|
||||
class StorageTypeFilter {
|
||||
|
||||
@Test
|
||||
@DisplayName("should return locations of given storage type")
|
||||
void shouldReturnByStorageType() {
|
||||
var coldRooms = List.of(
|
||||
activeLocation("Kühlraum", StorageType.COLD_ROOM)
|
||||
);
|
||||
when(storageLocationRepository.findByStorageType(StorageType.COLD_ROOM))
|
||||
.thenReturn(Result.success(coldRooms));
|
||||
|
||||
var result = listStorageLocations.execute("COLD_ROOM", null);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).hasSize(1);
|
||||
assertThat(result.unsafeGetValue().get(0).storageType()).isEqualTo(StorageType.COLD_ROOM);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with InvalidStorageType for unknown type")
|
||||
void shouldFailForInvalidStorageType() {
|
||||
var result = listStorageLocations.execute("INVALID", null);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(StorageLocationError.InvalidStorageType.class);
|
||||
verifyNoInteractions(storageLocationRepository);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Kombinierte Filter ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("kombinierte Filter (storageType + active)")
|
||||
class CombinedFilter {
|
||||
|
||||
@Test
|
||||
@DisplayName("should return only active locations of given type")
|
||||
void shouldReturnActiveOfType() {
|
||||
var coldRooms = List.of(
|
||||
activeLocation("Kühl Aktiv", StorageType.COLD_ROOM),
|
||||
inactiveLocation("Kühl Inaktiv", StorageType.COLD_ROOM)
|
||||
);
|
||||
when(storageLocationRepository.findByStorageType(StorageType.COLD_ROOM))
|
||||
.thenReturn(Result.success(coldRooms));
|
||||
|
||||
var result = listStorageLocations.execute("COLD_ROOM", true);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).hasSize(1);
|
||||
assertThat(result.unsafeGetValue().get(0).name().value()).isEqualTo("Kühl Aktiv");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return only inactive locations of given type")
|
||||
void shouldReturnInactiveOfType() {
|
||||
var coldRooms = List.of(
|
||||
activeLocation("Kühl Aktiv", StorageType.COLD_ROOM),
|
||||
inactiveLocation("Kühl Inaktiv", StorageType.COLD_ROOM)
|
||||
);
|
||||
when(storageLocationRepository.findByStorageType(StorageType.COLD_ROOM))
|
||||
.thenReturn(Result.success(coldRooms));
|
||||
|
||||
var result = listStorageLocations.execute("COLD_ROOM", false);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).hasSize(1);
|
||||
assertThat(result.unsafeGetValue().get(0).name().value()).isEqualTo("Kühl Inaktiv");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return empty list when no locations match combined filter")
|
||||
void shouldReturnEmptyWhenNoMatchCombined() {
|
||||
var coldRooms = List.of(
|
||||
activeLocation("Kühl Aktiv", StorageType.COLD_ROOM)
|
||||
);
|
||||
when(storageLocationRepository.findByStorageType(StorageType.COLD_ROOM))
|
||||
.thenReturn(Result.success(coldRooms));
|
||||
|
||||
var result = listStorageLocations.execute("COLD_ROOM", false);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).isEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,479 @@
|
|||
package de.effigenix.application.masterdata;
|
||||
|
||||
import de.effigenix.application.masterdata.command.CreateProductCategoryCommand;
|
||||
import de.effigenix.application.masterdata.command.UpdateProductCategoryCommand;
|
||||
import de.effigenix.domain.masterdata.*;
|
||||
import de.effigenix.shared.common.RepositoryError;
|
||||
import de.effigenix.shared.common.Result;
|
||||
import de.effigenix.shared.security.ActorId;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("ProductCategory Use Cases")
|
||||
class ProductCategoryUseCaseTest {
|
||||
|
||||
@Mock private ProductCategoryRepository categoryRepository;
|
||||
@Mock private ArticleRepository articleRepository;
|
||||
|
||||
private ActorId performedBy;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
performedBy = ActorId.of("admin-user");
|
||||
}
|
||||
|
||||
// ==================== Helpers ====================
|
||||
|
||||
private static ProductCategory existingCategory(String id, String name, String description) {
|
||||
return ProductCategory.reconstitute(
|
||||
ProductCategoryId.of(id),
|
||||
new CategoryName(name),
|
||||
description
|
||||
);
|
||||
}
|
||||
|
||||
private static ProductCategory existingCategory(String id, String name) {
|
||||
return existingCategory(id, name, "Beschreibung für " + name);
|
||||
}
|
||||
|
||||
// ==================== CreateProductCategory ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("CreateProductCategory")
|
||||
class CreateProductCategoryTests {
|
||||
|
||||
private CreateProductCategory useCase;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
useCase = new CreateProductCategory(categoryRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should create category successfully")
|
||||
void shouldCreateCategorySuccessfully() {
|
||||
var cmd = new CreateProductCategoryCommand("Backwaren", "Alle Backwaren");
|
||||
when(categoryRepository.existsByName(any(CategoryName.class)))
|
||||
.thenReturn(Result.success(false));
|
||||
when(categoryRepository.save(any(ProductCategory.class)))
|
||||
.thenReturn(Result.success(null));
|
||||
|
||||
var result = useCase.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().name().value()).isEqualTo("Backwaren");
|
||||
assertThat(result.unsafeGetValue().description()).isEqualTo("Alle Backwaren");
|
||||
verify(categoryRepository).existsByName(any(CategoryName.class));
|
||||
verify(categoryRepository).save(any(ProductCategory.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should create category with null description")
|
||||
void shouldCreateCategoryWithNullDescription() {
|
||||
var cmd = new CreateProductCategoryCommand("Backwaren", null);
|
||||
when(categoryRepository.existsByName(any(CategoryName.class)))
|
||||
.thenReturn(Result.success(false));
|
||||
when(categoryRepository.save(any(ProductCategory.class)))
|
||||
.thenReturn(Result.success(null));
|
||||
|
||||
var result = useCase.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().description()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with ValidationFailure when name is blank")
|
||||
void shouldFailWhenNameIsBlank() {
|
||||
var cmd = new CreateProductCategoryCommand(" ", "Beschreibung");
|
||||
|
||||
var result = useCase.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
|
||||
verifyNoInteractions(categoryRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with ValidationFailure when name is null")
|
||||
void shouldFailWhenNameIsNull() {
|
||||
var cmd = new CreateProductCategoryCommand(null, "Beschreibung");
|
||||
|
||||
var result = useCase.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
|
||||
verifyNoInteractions(categoryRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with ValidationFailure when name exceeds 100 characters")
|
||||
void shouldFailWhenNameTooLong() {
|
||||
var longName = "A".repeat(101);
|
||||
var cmd = new CreateProductCategoryCommand(longName, "Beschreibung");
|
||||
|
||||
var result = useCase.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
|
||||
verifyNoInteractions(categoryRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with CategoryNameAlreadyExists when name is taken")
|
||||
void shouldFailWhenNameAlreadyExists() {
|
||||
var cmd = new CreateProductCategoryCommand("Backwaren", "Beschreibung");
|
||||
when(categoryRepository.existsByName(any(CategoryName.class)))
|
||||
.thenReturn(Result.success(true));
|
||||
|
||||
var result = useCase.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.CategoryNameAlreadyExists.class);
|
||||
verify(categoryRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when existsByName fails")
|
||||
void shouldFailWhenExistsByNameFails() {
|
||||
var cmd = new CreateProductCategoryCommand("Backwaren", "Beschreibung");
|
||||
when(categoryRepository.existsByName(any(CategoryName.class)))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = useCase.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.RepositoryFailure.class);
|
||||
verify(categoryRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when save fails")
|
||||
void shouldFailWhenSaveFails() {
|
||||
var cmd = new CreateProductCategoryCommand("Backwaren", "Beschreibung");
|
||||
when(categoryRepository.existsByName(any(CategoryName.class)))
|
||||
.thenReturn(Result.success(false));
|
||||
when(categoryRepository.save(any(ProductCategory.class)))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
|
||||
|
||||
var result = useCase.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.RepositoryFailure.class);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== UpdateProductCategory ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("UpdateProductCategory")
|
||||
class UpdateProductCategoryTests {
|
||||
|
||||
private UpdateProductCategory useCase;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
useCase = new UpdateProductCategory(categoryRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update name and description successfully")
|
||||
void shouldUpdateNameAndDescription() {
|
||||
var categoryId = "cat-1";
|
||||
var cmd = new UpdateProductCategoryCommand(categoryId, "Neuer Name", "Neue Beschreibung");
|
||||
when(categoryRepository.findById(ProductCategoryId.of(categoryId)))
|
||||
.thenReturn(Result.success(Optional.of(existingCategory(categoryId, "Alter Name"))));
|
||||
when(categoryRepository.save(any(ProductCategory.class)))
|
||||
.thenReturn(Result.success(null));
|
||||
|
||||
var result = useCase.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().name().value()).isEqualTo("Neuer Name");
|
||||
assertThat(result.unsafeGetValue().description()).isEqualTo("Neue Beschreibung");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update only name when description is null")
|
||||
void shouldUpdateOnlyName() {
|
||||
var categoryId = "cat-1";
|
||||
var cmd = new UpdateProductCategoryCommand(categoryId, "Neuer Name", null);
|
||||
var original = existingCategory(categoryId, "Alter Name", "Originalbeschreibung");
|
||||
when(categoryRepository.findById(ProductCategoryId.of(categoryId)))
|
||||
.thenReturn(Result.success(Optional.of(original)));
|
||||
when(categoryRepository.save(any(ProductCategory.class)))
|
||||
.thenReturn(Result.success(null));
|
||||
|
||||
var result = useCase.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().name().value()).isEqualTo("Neuer Name");
|
||||
assertThat(result.unsafeGetValue().description()).isEqualTo("Originalbeschreibung");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update only description when name is null")
|
||||
void shouldUpdateOnlyDescription() {
|
||||
var categoryId = "cat-1";
|
||||
var cmd = new UpdateProductCategoryCommand(categoryId, null, "Neue Beschreibung");
|
||||
when(categoryRepository.findById(ProductCategoryId.of(categoryId)))
|
||||
.thenReturn(Result.success(Optional.of(existingCategory(categoryId, "Alter Name"))));
|
||||
when(categoryRepository.save(any(ProductCategory.class)))
|
||||
.thenReturn(Result.success(null));
|
||||
|
||||
var result = useCase.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().name().value()).isEqualTo("Alter Name");
|
||||
assertThat(result.unsafeGetValue().description()).isEqualTo("Neue Beschreibung");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with CategoryNotFound when category does not exist")
|
||||
void shouldFailWhenCategoryNotFound() {
|
||||
var cmd = new UpdateProductCategoryCommand("nonexistent", "Name", "Beschreibung");
|
||||
when(categoryRepository.findById(ProductCategoryId.of("nonexistent")))
|
||||
.thenReturn(Result.success(Optional.empty()));
|
||||
|
||||
var result = useCase.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.CategoryNotFound.class);
|
||||
verify(categoryRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with ValidationFailure when new name is blank")
|
||||
void shouldFailWhenNewNameIsBlank() {
|
||||
var categoryId = "cat-1";
|
||||
var cmd = new UpdateProductCategoryCommand(categoryId, " ", "Beschreibung");
|
||||
when(categoryRepository.findById(ProductCategoryId.of(categoryId)))
|
||||
.thenReturn(Result.success(Optional.of(existingCategory(categoryId, "Alter Name"))));
|
||||
|
||||
var result = useCase.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
|
||||
verify(categoryRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with ValidationFailure when new name exceeds 100 characters")
|
||||
void shouldFailWhenNewNameTooLong() {
|
||||
var categoryId = "cat-1";
|
||||
var longName = "B".repeat(101);
|
||||
var cmd = new UpdateProductCategoryCommand(categoryId, longName, null);
|
||||
when(categoryRepository.findById(ProductCategoryId.of(categoryId)))
|
||||
.thenReturn(Result.success(Optional.of(existingCategory(categoryId, "Alter Name"))));
|
||||
|
||||
var result = useCase.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
|
||||
verify(categoryRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when findById fails")
|
||||
void shouldFailWhenFindByIdFails() {
|
||||
var cmd = new UpdateProductCategoryCommand("cat-1", "Name", "Beschreibung");
|
||||
when(categoryRepository.findById(ProductCategoryId.of("cat-1")))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = useCase.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when save fails")
|
||||
void shouldFailWhenSaveFails() {
|
||||
var categoryId = "cat-1";
|
||||
var cmd = new UpdateProductCategoryCommand(categoryId, "Neuer Name", null);
|
||||
when(categoryRepository.findById(ProductCategoryId.of(categoryId)))
|
||||
.thenReturn(Result.success(Optional.of(existingCategory(categoryId, "Alter Name"))));
|
||||
when(categoryRepository.save(any(ProductCategory.class)))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
|
||||
|
||||
var result = useCase.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.RepositoryFailure.class);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== ListProductCategories ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("ListProductCategories")
|
||||
class ListProductCategoriesTests {
|
||||
|
||||
private ListProductCategories useCase;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
useCase = new ListProductCategories(categoryRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return all categories")
|
||||
void shouldReturnAllCategories() {
|
||||
var categories = List.of(
|
||||
existingCategory("cat-1", "Backwaren"),
|
||||
existingCategory("cat-2", "Getränke")
|
||||
);
|
||||
when(categoryRepository.findAll()).thenReturn(Result.success(categories));
|
||||
|
||||
var result = useCase.execute();
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return empty list when no categories exist")
|
||||
void shouldReturnEmptyList() {
|
||||
when(categoryRepository.findAll()).thenReturn(Result.success(List.of()));
|
||||
|
||||
var result = useCase.execute();
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when repository fails")
|
||||
void shouldFailWhenRepositoryFails() {
|
||||
when(categoryRepository.findAll())
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = useCase.execute();
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.RepositoryFailure.class);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== DeleteProductCategory ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("DeleteProductCategory")
|
||||
class DeleteProductCategoryTests {
|
||||
|
||||
private DeleteProductCategory useCase;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
useCase = new DeleteProductCategory(categoryRepository, articleRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should delete category successfully when not in use")
|
||||
void shouldDeleteCategorySuccessfully() {
|
||||
var categoryId = ProductCategoryId.of("cat-1");
|
||||
when(categoryRepository.findById(categoryId))
|
||||
.thenReturn(Result.success(Optional.of(existingCategory("cat-1", "Backwaren"))));
|
||||
when(articleRepository.findByCategory(categoryId))
|
||||
.thenReturn(Result.success(List.of()));
|
||||
when(categoryRepository.delete(any(ProductCategory.class)))
|
||||
.thenReturn(Result.success(null));
|
||||
|
||||
var result = useCase.execute(categoryId, performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
verify(categoryRepository).delete(any(ProductCategory.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with CategoryNotFound when category does not exist")
|
||||
void shouldFailWhenCategoryNotFound() {
|
||||
var categoryId = ProductCategoryId.of("nonexistent");
|
||||
when(categoryRepository.findById(categoryId))
|
||||
.thenReturn(Result.success(Optional.empty()));
|
||||
|
||||
var result = useCase.execute(categoryId, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.CategoryNotFound.class);
|
||||
verify(categoryRepository, never()).delete(any());
|
||||
verifyNoInteractions(articleRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with CategoryInUse when articles reference the category")
|
||||
void shouldFailWhenCategoryInUse() {
|
||||
var categoryId = ProductCategoryId.of("cat-1");
|
||||
when(categoryRepository.findById(categoryId))
|
||||
.thenReturn(Result.success(Optional.of(existingCategory("cat-1", "Backwaren"))));
|
||||
// Return a non-empty list (using mock since Article is complex)
|
||||
var mockArticle = mock(Article.class);
|
||||
when(articleRepository.findByCategory(categoryId))
|
||||
.thenReturn(Result.success(List.of(mockArticle)));
|
||||
|
||||
var result = useCase.execute(categoryId, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.CategoryInUse.class);
|
||||
verify(categoryRepository, never()).delete(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when findById fails")
|
||||
void shouldFailWhenFindByIdFails() {
|
||||
var categoryId = ProductCategoryId.of("cat-1");
|
||||
when(categoryRepository.findById(categoryId))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = useCase.execute(categoryId, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when findByCategory fails")
|
||||
void shouldFailWhenFindByCategoryFails() {
|
||||
var categoryId = ProductCategoryId.of("cat-1");
|
||||
when(categoryRepository.findById(categoryId))
|
||||
.thenReturn(Result.success(Optional.of(existingCategory("cat-1", "Backwaren"))));
|
||||
when(articleRepository.findByCategory(categoryId))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = useCase.execute(categoryId, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when delete fails")
|
||||
void shouldFailWhenDeleteFails() {
|
||||
var categoryId = ProductCategoryId.of("cat-1");
|
||||
when(categoryRepository.findById(categoryId))
|
||||
.thenReturn(Result.success(Optional.of(existingCategory("cat-1", "Backwaren"))));
|
||||
when(articleRepository.findByCategory(categoryId))
|
||||
.thenReturn(Result.success(List.of()));
|
||||
when(categoryRepository.delete(any(ProductCategory.class)))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
|
||||
|
||||
var result = useCase.execute(categoryId, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.RepositoryFailure.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,909 @@
|
|||
package de.effigenix.application.masterdata;
|
||||
|
||||
import de.effigenix.application.masterdata.command.*;
|
||||
import de.effigenix.domain.masterdata.*;
|
||||
import de.effigenix.shared.common.ContactInfo;
|
||||
import de.effigenix.shared.common.RepositoryError;
|
||||
import de.effigenix.shared.common.Result;
|
||||
import de.effigenix.shared.security.ActorId;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("Supplier Use Cases")
|
||||
class SupplierUseCaseTest {
|
||||
|
||||
@Mock private SupplierRepository supplierRepository;
|
||||
|
||||
private final ActorId performedBy = ActorId.of("admin-user");
|
||||
|
||||
// ==================== Helpers ====================
|
||||
|
||||
private static Supplier existingSupplier(String id) {
|
||||
return Supplier.reconstitute(
|
||||
SupplierId.of(id),
|
||||
new SupplierName("Test Supplier"),
|
||||
null,
|
||||
new ContactInfo("+49123456789", null, null),
|
||||
null,
|
||||
List.of(),
|
||||
null,
|
||||
SupplierStatus.ACTIVE,
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
OffsetDateTime.now(ZoneOffset.UTC)
|
||||
);
|
||||
}
|
||||
|
||||
private static Supplier existingSupplierWithCertificate(String id, QualityCertificate certificate) {
|
||||
return Supplier.reconstitute(
|
||||
SupplierId.of(id),
|
||||
new SupplierName("Test Supplier"),
|
||||
null,
|
||||
new ContactInfo("+49123456789", null, null),
|
||||
null,
|
||||
List.of(certificate),
|
||||
null,
|
||||
SupplierStatus.ACTIVE,
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
OffsetDateTime.now(ZoneOffset.UTC)
|
||||
);
|
||||
}
|
||||
|
||||
private static Supplier inactiveSupplier(String id) {
|
||||
return Supplier.reconstitute(
|
||||
SupplierId.of(id),
|
||||
new SupplierName("Inactive Supplier"),
|
||||
null,
|
||||
new ContactInfo("+49123456789", null, null),
|
||||
null,
|
||||
List.of(),
|
||||
null,
|
||||
SupplierStatus.INACTIVE,
|
||||
OffsetDateTime.now(ZoneOffset.UTC),
|
||||
OffsetDateTime.now(ZoneOffset.UTC)
|
||||
);
|
||||
}
|
||||
|
||||
private static CreateSupplierCommand validCreateCommand() {
|
||||
return new CreateSupplierCommand(
|
||||
"Acme Supplies", "+49123456789", "info@acme.de", "Max Mustermann",
|
||||
null, null, null, null, null,
|
||||
null, null
|
||||
);
|
||||
}
|
||||
|
||||
private static CreateSupplierCommand validCreateCommandWithAddress() {
|
||||
return new CreateSupplierCommand(
|
||||
"Acme Supplies", "+49123456789", "info@acme.de", "Max Mustermann",
|
||||
"Hauptstr.", "1", "12345", "Berlin", "DE",
|
||||
30, "30 Tage netto"
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== CreateSupplier ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("CreateSupplier")
|
||||
class CreateSupplierTest {
|
||||
|
||||
private CreateSupplier createSupplier;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
createSupplier = new CreateSupplier(supplierRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should create supplier with valid minimal data")
|
||||
void shouldCreateSupplierWithValidData() {
|
||||
when(supplierRepository.existsByName(any())).thenReturn(Result.success(false));
|
||||
when(supplierRepository.save(any())).thenReturn(Result.success(null));
|
||||
|
||||
var result = createSupplier.execute(validCreateCommand(), performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().name().value()).isEqualTo("Acme Supplies");
|
||||
assertThat(result.unsafeGetValue().status()).isEqualTo(SupplierStatus.ACTIVE);
|
||||
verify(supplierRepository).save(any(Supplier.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should create supplier with full data including address and payment terms")
|
||||
void shouldCreateSupplierWithFullData() {
|
||||
when(supplierRepository.existsByName(any())).thenReturn(Result.success(false));
|
||||
when(supplierRepository.save(any())).thenReturn(Result.success(null));
|
||||
|
||||
var result = createSupplier.execute(validCreateCommandWithAddress(), performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var supplier = result.unsafeGetValue();
|
||||
assertThat(supplier.address()).isNotNull();
|
||||
assertThat(supplier.paymentTerms()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when supplier name already exists")
|
||||
void shouldFailWhenNameAlreadyExists() {
|
||||
when(supplierRepository.existsByName(any())).thenReturn(Result.success(true));
|
||||
|
||||
var result = createSupplier.execute(validCreateCommand(), performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.SupplierNameAlreadyExists.class);
|
||||
verify(supplierRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with ValidationFailure when name is blank")
|
||||
void shouldFailWhenNameIsBlank() {
|
||||
var cmd = new CreateSupplierCommand(
|
||||
"", "+49123456789", null, null,
|
||||
null, null, null, null, null, null, null
|
||||
);
|
||||
|
||||
var result = createSupplier.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
|
||||
verify(supplierRepository, never()).existsByName(any());
|
||||
verify(supplierRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with ValidationFailure when email format is invalid")
|
||||
void shouldFailWhenEmailFormatIsInvalid() {
|
||||
var cmd = new CreateSupplierCommand(
|
||||
"Acme Supplies", "+49123456789", "invalid-email", null,
|
||||
null, null, null, null, null, null, null
|
||||
);
|
||||
|
||||
var result = createSupplier.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when existsByName fails")
|
||||
void shouldFailWhenExistsByNameFails() {
|
||||
when(supplierRepository.existsByName(any()))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = createSupplier.execute(validCreateCommand(), performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when save fails")
|
||||
void shouldFailWhenSaveFails() {
|
||||
when(supplierRepository.existsByName(any())).thenReturn(Result.success(false));
|
||||
when(supplierRepository.save(any()))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
|
||||
|
||||
var result = createSupplier.execute(validCreateCommand(), performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== UpdateSupplier ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("UpdateSupplier")
|
||||
class UpdateSupplierTest {
|
||||
|
||||
private UpdateSupplier updateSupplier;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
updateSupplier = new UpdateSupplier(supplierRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update supplier name")
|
||||
void shouldUpdateSupplierName() {
|
||||
var supplierId = "supplier-1";
|
||||
when(supplierRepository.findById(SupplierId.of(supplierId)))
|
||||
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
|
||||
when(supplierRepository.save(any())).thenReturn(Result.success(null));
|
||||
|
||||
var cmd = new UpdateSupplierCommand(
|
||||
supplierId, "New Name", null, null, null,
|
||||
null, null, null, null, null, null, null
|
||||
);
|
||||
|
||||
var result = updateSupplier.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().name().value()).isEqualTo("New Name");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with SupplierNotFound when supplier does not exist")
|
||||
void shouldFailWhenSupplierNotFound() {
|
||||
when(supplierRepository.findById(SupplierId.of("nonexistent")))
|
||||
.thenReturn(Result.success(Optional.empty()));
|
||||
|
||||
var cmd = new UpdateSupplierCommand(
|
||||
"nonexistent", "New Name", null, null, null,
|
||||
null, null, null, null, null, null, null
|
||||
);
|
||||
|
||||
var result = updateSupplier.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.SupplierNotFound.class);
|
||||
verify(supplierRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with ValidationFailure when new name is blank")
|
||||
void shouldFailWhenNewNameIsBlank() {
|
||||
var supplierId = "supplier-1";
|
||||
when(supplierRepository.findById(SupplierId.of(supplierId)))
|
||||
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
|
||||
|
||||
var cmd = new UpdateSupplierCommand(
|
||||
supplierId, "", null, null, null,
|
||||
null, null, null, null, null, null, null
|
||||
);
|
||||
|
||||
var result = updateSupplier.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
|
||||
verify(supplierRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when findById fails")
|
||||
void shouldFailWhenFindByIdFails() {
|
||||
when(supplierRepository.findById(any()))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var cmd = new UpdateSupplierCommand(
|
||||
"supplier-1", "New Name", null, null, null,
|
||||
null, null, null, null, null, null, null
|
||||
);
|
||||
|
||||
var result = updateSupplier.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when save fails")
|
||||
void shouldFailWhenSaveFails() {
|
||||
var supplierId = "supplier-1";
|
||||
when(supplierRepository.findById(SupplierId.of(supplierId)))
|
||||
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
|
||||
when(supplierRepository.save(any()))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
|
||||
|
||||
var cmd = new UpdateSupplierCommand(
|
||||
supplierId, "New Name", null, null, null,
|
||||
null, null, null, null, null, null, null
|
||||
);
|
||||
|
||||
var result = updateSupplier.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== GetSupplier ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("GetSupplier")
|
||||
class GetSupplierTest {
|
||||
|
||||
private GetSupplier getSupplier;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
getSupplier = new GetSupplier(supplierRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return supplier when found")
|
||||
void shouldReturnSupplierWhenFound() {
|
||||
var supplierId = SupplierId.of("supplier-1");
|
||||
when(supplierRepository.findById(supplierId))
|
||||
.thenReturn(Result.success(Optional.of(existingSupplier("supplier-1"))));
|
||||
|
||||
var result = getSupplier.execute(supplierId);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().id()).isEqualTo(supplierId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with SupplierNotFound when not found")
|
||||
void shouldFailWhenNotFound() {
|
||||
var supplierId = SupplierId.of("nonexistent");
|
||||
when(supplierRepository.findById(supplierId))
|
||||
.thenReturn(Result.success(Optional.empty()));
|
||||
|
||||
var result = getSupplier.execute(supplierId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.SupplierNotFound.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when repository fails")
|
||||
void shouldFailWhenRepositoryFails() {
|
||||
var supplierId = SupplierId.of("supplier-1");
|
||||
when(supplierRepository.findById(supplierId))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = getSupplier.execute(supplierId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== ListSuppliers ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("ListSuppliers")
|
||||
class ListSuppliersTest {
|
||||
|
||||
private ListSuppliers listSuppliers;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
listSuppliers = new ListSuppliers(supplierRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return all suppliers")
|
||||
void shouldReturnAllSuppliers() {
|
||||
var suppliers = List.of(existingSupplier("s-1"), existingSupplier("s-2"));
|
||||
when(supplierRepository.findAll()).thenReturn(Result.success(suppliers));
|
||||
|
||||
var result = listSuppliers.execute();
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return empty list when no suppliers exist")
|
||||
void shouldReturnEmptyList() {
|
||||
when(supplierRepository.findAll()).thenReturn(Result.success(List.of()));
|
||||
|
||||
var result = listSuppliers.execute();
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when findAll fails")
|
||||
void shouldFailWhenFindAllFails() {
|
||||
when(supplierRepository.findAll())
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = listSuppliers.execute();
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return suppliers filtered by status")
|
||||
void shouldReturnSuppliersByStatus() {
|
||||
var activeSuppliers = List.of(existingSupplier("s-1"));
|
||||
when(supplierRepository.findByStatus(SupplierStatus.ACTIVE))
|
||||
.thenReturn(Result.success(activeSuppliers));
|
||||
|
||||
var result = listSuppliers.executeByStatus(SupplierStatus.ACTIVE);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when findByStatus fails")
|
||||
void shouldFailWhenFindByStatusFails() {
|
||||
when(supplierRepository.findByStatus(SupplierStatus.ACTIVE))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = listSuppliers.executeByStatus(SupplierStatus.ACTIVE);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== ActivateSupplier ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("ActivateSupplier")
|
||||
class ActivateSupplierTest {
|
||||
|
||||
private ActivateSupplier activateSupplier;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
activateSupplier = new ActivateSupplier(supplierRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should activate an inactive supplier")
|
||||
void shouldActivateInactiveSupplier() {
|
||||
var supplierId = SupplierId.of("supplier-1");
|
||||
when(supplierRepository.findById(supplierId))
|
||||
.thenReturn(Result.success(Optional.of(inactiveSupplier("supplier-1"))));
|
||||
when(supplierRepository.save(any())).thenReturn(Result.success(null));
|
||||
|
||||
var result = activateSupplier.execute(supplierId, performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().status()).isEqualTo(SupplierStatus.ACTIVE);
|
||||
verify(supplierRepository).save(any(Supplier.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with SupplierNotFound when supplier does not exist")
|
||||
void shouldFailWhenSupplierNotFound() {
|
||||
var supplierId = SupplierId.of("nonexistent");
|
||||
when(supplierRepository.findById(supplierId))
|
||||
.thenReturn(Result.success(Optional.empty()));
|
||||
|
||||
var result = activateSupplier.execute(supplierId, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.SupplierNotFound.class);
|
||||
verify(supplierRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when findById fails")
|
||||
void shouldFailWhenFindByIdFails() {
|
||||
var supplierId = SupplierId.of("supplier-1");
|
||||
when(supplierRepository.findById(supplierId))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = activateSupplier.execute(supplierId, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when save fails")
|
||||
void shouldFailWhenSaveFails() {
|
||||
var supplierId = SupplierId.of("supplier-1");
|
||||
when(supplierRepository.findById(supplierId))
|
||||
.thenReturn(Result.success(Optional.of(inactiveSupplier("supplier-1"))));
|
||||
when(supplierRepository.save(any()))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
|
||||
|
||||
var result = activateSupplier.execute(supplierId, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== DeactivateSupplier ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("DeactivateSupplier")
|
||||
class DeactivateSupplierTest {
|
||||
|
||||
private DeactivateSupplier deactivateSupplier;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
deactivateSupplier = new DeactivateSupplier(supplierRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should deactivate an active supplier")
|
||||
void shouldDeactivateActiveSupplier() {
|
||||
var supplierId = SupplierId.of("supplier-1");
|
||||
when(supplierRepository.findById(supplierId))
|
||||
.thenReturn(Result.success(Optional.of(existingSupplier("supplier-1"))));
|
||||
when(supplierRepository.save(any())).thenReturn(Result.success(null));
|
||||
|
||||
var result = deactivateSupplier.execute(supplierId, performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().status()).isEqualTo(SupplierStatus.INACTIVE);
|
||||
verify(supplierRepository).save(any(Supplier.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with SupplierNotFound when supplier does not exist")
|
||||
void shouldFailWhenSupplierNotFound() {
|
||||
var supplierId = SupplierId.of("nonexistent");
|
||||
when(supplierRepository.findById(supplierId))
|
||||
.thenReturn(Result.success(Optional.empty()));
|
||||
|
||||
var result = deactivateSupplier.execute(supplierId, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.SupplierNotFound.class);
|
||||
verify(supplierRepository, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when findById fails")
|
||||
void shouldFailWhenFindByIdFails() {
|
||||
var supplierId = SupplierId.of("supplier-1");
|
||||
when(supplierRepository.findById(supplierId))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = deactivateSupplier.execute(supplierId, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when save fails")
|
||||
void shouldFailWhenSaveFails() {
|
||||
var supplierId = SupplierId.of("supplier-1");
|
||||
when(supplierRepository.findById(supplierId))
|
||||
.thenReturn(Result.success(Optional.of(existingSupplier("supplier-1"))));
|
||||
when(supplierRepository.save(any()))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
|
||||
|
||||
var result = deactivateSupplier.execute(supplierId, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== RateSupplier ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("RateSupplier")
|
||||
class RateSupplierTest {
|
||||
|
||||
private RateSupplier rateSupplier;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
rateSupplier = new RateSupplier(supplierRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should rate supplier with valid scores")
|
||||
void shouldRateSupplierWithValidScores() {
|
||||
var supplierId = "supplier-1";
|
||||
when(supplierRepository.findById(SupplierId.of(supplierId)))
|
||||
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
|
||||
when(supplierRepository.save(any())).thenReturn(Result.success(null));
|
||||
|
||||
var cmd = new RateSupplierCommand(supplierId, 4, 5, 3);
|
||||
|
||||
var result = rateSupplier.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var rating = result.unsafeGetValue().rating();
|
||||
assertThat(rating).isNotNull();
|
||||
assertThat(rating.qualityScore()).isEqualTo(4);
|
||||
assertThat(rating.deliveryScore()).isEqualTo(5);
|
||||
assertThat(rating.priceScore()).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with SupplierNotFound when supplier does not exist")
|
||||
void shouldFailWhenSupplierNotFound() {
|
||||
when(supplierRepository.findById(SupplierId.of("nonexistent")))
|
||||
.thenReturn(Result.success(Optional.empty()));
|
||||
|
||||
var cmd = new RateSupplierCommand("nonexistent", 4, 5, 3);
|
||||
|
||||
var result = rateSupplier.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.SupplierNotFound.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with InvalidRating when score is out of range")
|
||||
void shouldFailWhenScoreIsOutOfRange() {
|
||||
var supplierId = "supplier-1";
|
||||
when(supplierRepository.findById(SupplierId.of(supplierId)))
|
||||
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
|
||||
|
||||
var cmd = new RateSupplierCommand(supplierId, 0, 5, 3);
|
||||
|
||||
var result = rateSupplier.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.InvalidRating.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with InvalidRating when score exceeds maximum")
|
||||
void shouldFailWhenScoreExceedsMax() {
|
||||
var supplierId = "supplier-1";
|
||||
when(supplierRepository.findById(SupplierId.of(supplierId)))
|
||||
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
|
||||
|
||||
var cmd = new RateSupplierCommand(supplierId, 4, 6, 3);
|
||||
|
||||
var result = rateSupplier.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.InvalidRating.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when findById fails")
|
||||
void shouldFailWhenFindByIdFails() {
|
||||
when(supplierRepository.findById(any()))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var cmd = new RateSupplierCommand("supplier-1", 4, 5, 3);
|
||||
|
||||
var result = rateSupplier.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when save fails")
|
||||
void shouldFailWhenSaveFails() {
|
||||
var supplierId = "supplier-1";
|
||||
when(supplierRepository.findById(SupplierId.of(supplierId)))
|
||||
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
|
||||
when(supplierRepository.save(any()))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
|
||||
|
||||
var cmd = new RateSupplierCommand(supplierId, 4, 5, 3);
|
||||
|
||||
var result = rateSupplier.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== AddCertificate ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("AddCertificate")
|
||||
class AddCertificateTest {
|
||||
|
||||
private AddCertificate addCertificate;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
addCertificate = new AddCertificate(supplierRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should add certificate to supplier")
|
||||
void shouldAddCertificateToSupplier() {
|
||||
var supplierId = "supplier-1";
|
||||
when(supplierRepository.findById(SupplierId.of(supplierId)))
|
||||
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
|
||||
when(supplierRepository.save(any())).thenReturn(Result.success(null));
|
||||
|
||||
var cmd = new AddCertificateCommand(
|
||||
supplierId, "ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2027, 12, 31)
|
||||
);
|
||||
|
||||
var result = addCertificate.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().certificates()).hasSize(1);
|
||||
assertThat(result.unsafeGetValue().certificates().getFirst().certificateType()).isEqualTo("ISO 9001");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with SupplierNotFound when supplier does not exist")
|
||||
void shouldFailWhenSupplierNotFound() {
|
||||
when(supplierRepository.findById(SupplierId.of("nonexistent")))
|
||||
.thenReturn(Result.success(Optional.empty()));
|
||||
|
||||
var cmd = new AddCertificateCommand(
|
||||
"nonexistent", "ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2027, 12, 31)
|
||||
);
|
||||
|
||||
var result = addCertificate.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.SupplierNotFound.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with ValidationFailure when certificate type is blank")
|
||||
void shouldFailWhenCertificateTypeIsBlank() {
|
||||
var supplierId = "supplier-1";
|
||||
when(supplierRepository.findById(SupplierId.of(supplierId)))
|
||||
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
|
||||
|
||||
var cmd = new AddCertificateCommand(
|
||||
supplierId, "", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2027, 12, 31)
|
||||
);
|
||||
|
||||
var result = addCertificate.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with ValidationFailure when validUntil is before validFrom")
|
||||
void shouldFailWhenValidUntilBeforeValidFrom() {
|
||||
var supplierId = "supplier-1";
|
||||
when(supplierRepository.findById(SupplierId.of(supplierId)))
|
||||
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
|
||||
|
||||
var cmd = new AddCertificateCommand(
|
||||
supplierId, "ISO 9001", "TUV", LocalDate.of(2027, 1, 1), LocalDate.of(2025, 12, 31)
|
||||
);
|
||||
|
||||
var result = addCertificate.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when findById fails")
|
||||
void shouldFailWhenFindByIdFails() {
|
||||
when(supplierRepository.findById(any()))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var cmd = new AddCertificateCommand(
|
||||
"supplier-1", "ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2027, 12, 31)
|
||||
);
|
||||
|
||||
var result = addCertificate.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when save fails")
|
||||
void shouldFailWhenSaveFails() {
|
||||
var supplierId = "supplier-1";
|
||||
when(supplierRepository.findById(SupplierId.of(supplierId)))
|
||||
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
|
||||
when(supplierRepository.save(any()))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
|
||||
|
||||
var cmd = new AddCertificateCommand(
|
||||
supplierId, "ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2027, 12, 31)
|
||||
);
|
||||
|
||||
var result = addCertificate.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== RemoveCertificate ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("RemoveCertificate")
|
||||
class RemoveCertificateTest {
|
||||
|
||||
private RemoveCertificate removeCertificate;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
removeCertificate = new RemoveCertificate(supplierRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should remove existing certificate from supplier")
|
||||
void shouldRemoveExistingCertificate() {
|
||||
var supplierId = "supplier-1";
|
||||
var validFrom = LocalDate.of(2025, 1, 1);
|
||||
var certificate = new QualityCertificate("ISO 9001", "TUV", validFrom, LocalDate.of(2027, 12, 31));
|
||||
when(supplierRepository.findById(SupplierId.of(supplierId)))
|
||||
.thenReturn(Result.success(Optional.of(existingSupplierWithCertificate(supplierId, certificate))));
|
||||
when(supplierRepository.save(any())).thenReturn(Result.success(null));
|
||||
|
||||
var cmd = new RemoveCertificateCommand(supplierId, "ISO 9001", "TUV", validFrom);
|
||||
|
||||
var result = removeCertificate.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().certificates()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with CertificateNotFound when certificate does not exist")
|
||||
void shouldFailWhenCertificateNotFound() {
|
||||
var supplierId = "supplier-1";
|
||||
when(supplierRepository.findById(SupplierId.of(supplierId)))
|
||||
.thenReturn(Result.success(Optional.of(existingSupplier(supplierId))));
|
||||
|
||||
var cmd = new RemoveCertificateCommand(
|
||||
supplierId, "ISO 9001", "TUV", LocalDate.of(2025, 1, 1)
|
||||
);
|
||||
|
||||
var result = removeCertificate.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.CertificateNotFound.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with SupplierNotFound when supplier does not exist")
|
||||
void shouldFailWhenSupplierNotFound() {
|
||||
when(supplierRepository.findById(SupplierId.of("nonexistent")))
|
||||
.thenReturn(Result.success(Optional.empty()));
|
||||
|
||||
var cmd = new RemoveCertificateCommand(
|
||||
"nonexistent", "ISO 9001", "TUV", LocalDate.of(2025, 1, 1)
|
||||
);
|
||||
|
||||
var result = removeCertificate.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.SupplierNotFound.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when findById fails")
|
||||
void shouldFailWhenFindByIdFails() {
|
||||
when(supplierRepository.findById(any()))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var cmd = new RemoveCertificateCommand(
|
||||
"supplier-1", "ISO 9001", "TUV", LocalDate.of(2025, 1, 1)
|
||||
);
|
||||
|
||||
var result = removeCertificate.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with RepositoryFailure when save fails")
|
||||
void shouldFailWhenSaveFails() {
|
||||
var supplierId = "supplier-1";
|
||||
var validFrom = LocalDate.of(2025, 1, 1);
|
||||
var certificate = new QualityCertificate("ISO 9001", "TUV", validFrom, LocalDate.of(2027, 12, 31));
|
||||
when(supplierRepository.findById(SupplierId.of(supplierId)))
|
||||
.thenReturn(Result.success(Optional.of(existingSupplierWithCertificate(supplierId, certificate))));
|
||||
when(supplierRepository.save(any()))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("disk full")));
|
||||
|
||||
var cmd = new RemoveCertificateCommand(supplierId, "ISO 9001", "TUV", validFrom);
|
||||
|
||||
var result = removeCertificate.execute(cmd, performedBy);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,730 @@
|
|||
package de.effigenix.domain.masterdata;
|
||||
|
||||
import de.effigenix.shared.common.Money;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ArticleTest {
|
||||
|
||||
// ==================== Create ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("create()")
|
||||
class Create {
|
||||
|
||||
@Test
|
||||
@DisplayName("should create Article with valid data (PIECE_FIXED / FIXED)")
|
||||
void shouldCreateWithValidData() {
|
||||
var draft = validDraft();
|
||||
|
||||
var result = Article.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var article = result.unsafeGetValue();
|
||||
assertThat(article.id()).isNotNull();
|
||||
assertThat(article.name().value()).isEqualTo("Testwurst");
|
||||
assertThat(article.articleNumber().value()).isEqualTo("ART-001");
|
||||
assertThat(article.categoryId()).isNotNull();
|
||||
assertThat(article.status()).isEqualTo(ArticleStatus.ACTIVE);
|
||||
assertThat(article.salesUnits()).hasSize(1);
|
||||
assertThat(article.salesUnits().getFirst().unit()).isEqualTo(Unit.PIECE_FIXED);
|
||||
assertThat(article.salesUnits().getFirst().priceModel()).isEqualTo(PriceModel.FIXED);
|
||||
assertThat(article.salesUnits().getFirst().price().amount()).isEqualByComparingTo("9.99");
|
||||
assertThat(article.supplierReferences()).isEmpty();
|
||||
assertThat(article.createdAt()).isNotNull();
|
||||
assertThat(article.updatedAt()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should create Article with KG / WEIGHT_BASED")
|
||||
void shouldCreateWithKgWeightBased() {
|
||||
var draft = new ArticleDraft("Hackfleisch", "ART-002", UUID.randomUUID().toString(),
|
||||
Unit.KG, PriceModel.WEIGHT_BASED, new BigDecimal("12.50"));
|
||||
|
||||
var result = Article.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var su = result.unsafeGetValue().salesUnits().getFirst();
|
||||
assertThat(su.unit()).isEqualTo(Unit.KG);
|
||||
assertThat(su.priceModel()).isEqualTo(PriceModel.WEIGHT_BASED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should create Article with HUNDRED_GRAM / WEIGHT_BASED")
|
||||
void shouldCreateWithHundredGramWeightBased() {
|
||||
var draft = new ArticleDraft("Aufschnitt", "ART-003", UUID.randomUUID().toString(),
|
||||
Unit.HUNDRED_GRAM, PriceModel.WEIGHT_BASED, new BigDecimal("1.99"));
|
||||
|
||||
var result = Article.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should create Article with PIECE_VARIABLE / WEIGHT_BASED")
|
||||
void shouldCreateWithPieceVariableWeightBased() {
|
||||
var draft = new ArticleDraft("Braten", "ART-004", UUID.randomUUID().toString(),
|
||||
Unit.PIECE_VARIABLE, PriceModel.WEIGHT_BASED, new BigDecimal("15.00"));
|
||||
|
||||
var result = Article.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when name is blank")
|
||||
void shouldFailWhenNameBlank() {
|
||||
var draft = new ArticleDraft("", "ART-001", UUID.randomUUID().toString(),
|
||||
Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));
|
||||
|
||||
var result = Article.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when name is null")
|
||||
void shouldFailWhenNameNull() {
|
||||
var draft = new ArticleDraft(null, "ART-001", UUID.randomUUID().toString(),
|
||||
Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));
|
||||
|
||||
var result = Article.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when name exceeds 200 characters")
|
||||
void shouldFailWhenNameTooLong() {
|
||||
var longName = "A".repeat(201);
|
||||
var draft = new ArticleDraft(longName, "ART-001", UUID.randomUUID().toString(),
|
||||
Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));
|
||||
|
||||
var result = Article.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should accept name with exactly 200 characters")
|
||||
void shouldAcceptNameWith200Chars() {
|
||||
var name = "A".repeat(200);
|
||||
var draft = new ArticleDraft(name, "ART-001", UUID.randomUUID().toString(),
|
||||
Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));
|
||||
|
||||
var result = Article.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when articleNumber is blank")
|
||||
void shouldFailWhenArticleNumberBlank() {
|
||||
var draft = new ArticleDraft("Testwurst", "", UUID.randomUUID().toString(),
|
||||
Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));
|
||||
|
||||
var result = Article.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when articleNumber is null")
|
||||
void shouldFailWhenArticleNumberNull() {
|
||||
var draft = new ArticleDraft("Testwurst", null, UUID.randomUUID().toString(),
|
||||
Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));
|
||||
|
||||
var result = Article.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when articleNumber exceeds 50 characters")
|
||||
void shouldFailWhenArticleNumberTooLong() {
|
||||
var longNumber = "A".repeat(51);
|
||||
var draft = new ArticleDraft("Testwurst", longNumber, UUID.randomUUID().toString(),
|
||||
Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));
|
||||
|
||||
var result = Article.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should accept articleNumber with exactly 50 characters")
|
||||
void shouldAcceptArticleNumberWith50Chars() {
|
||||
var number = "A".repeat(50);
|
||||
var draft = new ArticleDraft("Testwurst", number, UUID.randomUUID().toString(),
|
||||
Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("9.99"));
|
||||
|
||||
var result = Article.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should throw when price is null (Money.tryEuro does not guard null)")
|
||||
void shouldThrowWhenPriceNull() {
|
||||
var draft = new ArticleDraft("Testwurst", "ART-001", UUID.randomUUID().toString(),
|
||||
Unit.PIECE_FIXED, PriceModel.FIXED, null);
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertThrows(NullPointerException.class,
|
||||
() -> Article.create(draft));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when price is zero")
|
||||
void shouldFailWhenPriceZero() {
|
||||
var draft = new ArticleDraft("Testwurst", "ART-001", UUID.randomUUID().toString(),
|
||||
Unit.PIECE_FIXED, PriceModel.FIXED, BigDecimal.ZERO);
|
||||
|
||||
var result = Article.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.InvalidPrice.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when price is negative")
|
||||
void shouldFailWhenPriceNegative() {
|
||||
var draft = new ArticleDraft("Testwurst", "ART-001", UUID.randomUUID().toString(),
|
||||
Unit.PIECE_FIXED, PriceModel.FIXED, new BigDecimal("-1.00"));
|
||||
|
||||
var result = Article.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when Unit/PriceModel combination is invalid (PIECE_FIXED + WEIGHT_BASED)")
|
||||
void shouldFailWhenInvalidCombinationPieceFixedWeightBased() {
|
||||
var draft = new ArticleDraft("Testwurst", "ART-001", UUID.randomUUID().toString(),
|
||||
Unit.PIECE_FIXED, PriceModel.WEIGHT_BASED, new BigDecimal("9.99"));
|
||||
|
||||
var result = Article.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.InvalidPriceModelCombination.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when Unit/PriceModel combination is invalid (KG + FIXED)")
|
||||
void shouldFailWhenInvalidCombinationKgFixed() {
|
||||
var draft = new ArticleDraft("Testwurst", "ART-001", UUID.randomUUID().toString(),
|
||||
Unit.KG, PriceModel.FIXED, new BigDecimal("9.99"));
|
||||
|
||||
var result = Article.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.InvalidPriceModelCombination.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when unit is null")
|
||||
void shouldFailWhenUnitNull() {
|
||||
var draft = new ArticleDraft("Testwurst", "ART-001", UUID.randomUUID().toString(),
|
||||
null, PriceModel.FIXED, new BigDecimal("9.99"));
|
||||
|
||||
var result = Article.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.InvalidPriceModelCombination.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when priceModel is null")
|
||||
void shouldFailWhenPriceModelNull() {
|
||||
var draft = new ArticleDraft("Testwurst", "ART-001", UUID.randomUUID().toString(),
|
||||
Unit.PIECE_FIXED, null, new BigDecimal("9.99"));
|
||||
|
||||
var result = Article.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.InvalidPriceModelCombination.class);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Update ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("update()")
|
||||
class Update {
|
||||
|
||||
@Test
|
||||
@DisplayName("should update name")
|
||||
void shouldUpdateName() {
|
||||
var article = createValidArticle();
|
||||
var updateDraft = new ArticleUpdateDraft("Neuer Name", null);
|
||||
|
||||
var result = article.update(updateDraft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(article.name().value()).isEqualTo("Neuer Name");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update categoryId")
|
||||
void shouldUpdateCategoryId() {
|
||||
var article = createValidArticle();
|
||||
var newCategoryId = UUID.randomUUID().toString();
|
||||
var updateDraft = new ArticleUpdateDraft(null, newCategoryId);
|
||||
|
||||
var result = article.update(updateDraft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(article.categoryId().value()).isEqualTo(newCategoryId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update both name and categoryId")
|
||||
void shouldUpdateBothFields() {
|
||||
var article = createValidArticle();
|
||||
var newCategoryId = UUID.randomUUID().toString();
|
||||
var updateDraft = new ArticleUpdateDraft("Neuer Name", newCategoryId);
|
||||
|
||||
var result = article.update(updateDraft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(article.name().value()).isEqualTo("Neuer Name");
|
||||
assertThat(article.categoryId().value()).isEqualTo(newCategoryId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not change fields when all null in draft")
|
||||
void shouldNotChangeWhenAllNull() {
|
||||
var article = createValidArticle();
|
||||
var originalName = article.name().value();
|
||||
var originalCategoryId = article.categoryId().value();
|
||||
var updateDraft = new ArticleUpdateDraft(null, null);
|
||||
|
||||
var result = article.update(updateDraft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(article.name().value()).isEqualTo(originalName);
|
||||
assertThat(article.categoryId().value()).isEqualTo(originalCategoryId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when updated name is blank")
|
||||
void shouldFailWhenUpdatedNameBlank() {
|
||||
var article = createValidArticle();
|
||||
var updateDraft = new ArticleUpdateDraft("", null);
|
||||
|
||||
var result = article.update(updateDraft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when updated name exceeds 200 characters")
|
||||
void shouldFailWhenUpdatedNameTooLong() {
|
||||
var article = createValidArticle();
|
||||
var updateDraft = new ArticleUpdateDraft("A".repeat(201), null);
|
||||
|
||||
var result = article.update(updateDraft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update updatedAt timestamp")
|
||||
void shouldUpdateTimestamp() {
|
||||
var article = createValidArticle();
|
||||
var originalUpdatedAt = article.updatedAt();
|
||||
|
||||
article.update(new ArticleUpdateDraft("Anderer Name", null));
|
||||
|
||||
assertThat(article.updatedAt()).isAfterOrEqualTo(originalUpdatedAt);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Activate / Deactivate ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("activate() / deactivate()")
|
||||
class ActivateDeactivate {
|
||||
|
||||
@Test
|
||||
@DisplayName("should deactivate active article")
|
||||
void shouldDeactivate() {
|
||||
var article = createValidArticle();
|
||||
assertThat(article.status()).isEqualTo(ArticleStatus.ACTIVE);
|
||||
|
||||
article.deactivate();
|
||||
|
||||
assertThat(article.status()).isEqualTo(ArticleStatus.INACTIVE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should activate inactive article")
|
||||
void shouldActivate() {
|
||||
var article = createValidArticle();
|
||||
article.deactivate();
|
||||
|
||||
article.activate();
|
||||
|
||||
assertThat(article.status()).isEqualTo(ArticleStatus.ACTIVE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update updatedAt on deactivate")
|
||||
void shouldUpdateTimestampOnDeactivate() {
|
||||
var article = createValidArticle();
|
||||
var originalUpdatedAt = article.updatedAt();
|
||||
|
||||
article.deactivate();
|
||||
|
||||
assertThat(article.updatedAt()).isAfterOrEqualTo(originalUpdatedAt);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update updatedAt on activate")
|
||||
void shouldUpdateTimestampOnActivate() {
|
||||
var article = createValidArticle();
|
||||
article.deactivate();
|
||||
var afterDeactivate = article.updatedAt();
|
||||
|
||||
article.activate();
|
||||
|
||||
assertThat(article.updatedAt()).isAfterOrEqualTo(afterDeactivate);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Add Sales Unit ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("addSalesUnit()")
|
||||
class AddSalesUnit {
|
||||
|
||||
@Test
|
||||
@DisplayName("should add a second SalesUnit with different unit type")
|
||||
void shouldAddSecondSalesUnit() {
|
||||
var article = createValidArticle();
|
||||
var salesUnit = SalesUnit.create(Unit.KG, PriceModel.WEIGHT_BASED, Money.euro(new BigDecimal("12.50")))
|
||||
.unsafeGetValue();
|
||||
|
||||
var result = article.addSalesUnit(salesUnit);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(article.salesUnits()).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when adding SalesUnit with duplicate unit type")
|
||||
void shouldFailWhenDuplicateUnitType() {
|
||||
var article = createValidArticle();
|
||||
var duplicate = SalesUnit.create(Unit.PIECE_FIXED, PriceModel.FIXED, Money.euro(new BigDecimal("5.00")))
|
||||
.unsafeGetValue();
|
||||
|
||||
var result = article.addSalesUnit(duplicate);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.DuplicateSalesUnitType.class);
|
||||
assertThat(article.salesUnits()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update updatedAt on add")
|
||||
void shouldUpdateTimestampOnAdd() {
|
||||
var article = createValidArticle();
|
||||
var originalUpdatedAt = article.updatedAt();
|
||||
var salesUnit = SalesUnit.create(Unit.KG, PriceModel.WEIGHT_BASED, Money.euro(new BigDecimal("12.50")))
|
||||
.unsafeGetValue();
|
||||
|
||||
article.addSalesUnit(salesUnit);
|
||||
|
||||
assertThat(article.updatedAt()).isAfterOrEqualTo(originalUpdatedAt);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Remove Sales Unit ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("removeSalesUnit()")
|
||||
class RemoveSalesUnit {
|
||||
|
||||
@Test
|
||||
@DisplayName("should remove SalesUnit when more than one exists")
|
||||
void shouldRemoveWhenMultiple() {
|
||||
var article = createValidArticle();
|
||||
var secondUnit = SalesUnit.create(Unit.KG, PriceModel.WEIGHT_BASED, Money.euro(new BigDecimal("12.50")))
|
||||
.unsafeGetValue();
|
||||
article.addSalesUnit(secondUnit);
|
||||
|
||||
var result = article.removeSalesUnit(secondUnit.id());
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(article.salesUnits()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when removing the last SalesUnit")
|
||||
void shouldFailWhenRemovingLastSalesUnit() {
|
||||
var article = createValidArticle();
|
||||
var onlySalesUnitId = article.salesUnits().getFirst().id();
|
||||
|
||||
var result = article.removeSalesUnit(onlySalesUnitId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.MinimumSalesUnitRequired.class);
|
||||
assertThat(article.salesUnits()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when SalesUnit ID does not exist")
|
||||
void shouldFailWhenSalesUnitNotFound() {
|
||||
var article = createValidArticle();
|
||||
var secondUnit = SalesUnit.create(Unit.KG, PriceModel.WEIGHT_BASED, Money.euro(new BigDecimal("12.50")))
|
||||
.unsafeGetValue();
|
||||
article.addSalesUnit(secondUnit);
|
||||
|
||||
var nonExistentId = SalesUnitId.generate();
|
||||
var result = article.removeSalesUnit(nonExistentId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.SalesUnitNotFound.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update updatedAt on remove")
|
||||
void shouldUpdateTimestampOnRemove() {
|
||||
var article = createValidArticle();
|
||||
var secondUnit = SalesUnit.create(Unit.KG, PriceModel.WEIGHT_BASED, Money.euro(new BigDecimal("12.50")))
|
||||
.unsafeGetValue();
|
||||
article.addSalesUnit(secondUnit);
|
||||
var afterAdd = article.updatedAt();
|
||||
|
||||
article.removeSalesUnit(secondUnit.id());
|
||||
|
||||
assertThat(article.updatedAt()).isAfterOrEqualTo(afterAdd);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Update Sales Unit Price ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("updateSalesUnitPrice()")
|
||||
class UpdateSalesUnitPrice {
|
||||
|
||||
@Test
|
||||
@DisplayName("should update price of existing SalesUnit")
|
||||
void shouldUpdatePrice() {
|
||||
var article = createValidArticle();
|
||||
var salesUnitId = article.salesUnits().getFirst().id();
|
||||
var newPrice = Money.euro(new BigDecimal("19.99"));
|
||||
|
||||
var result = article.updateSalesUnitPrice(salesUnitId, newPrice);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(article.salesUnits().getFirst().price().amount()).isEqualByComparingTo("19.99");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when SalesUnit not found")
|
||||
void shouldFailWhenNotFound() {
|
||||
var article = createValidArticle();
|
||||
var nonExistentId = SalesUnitId.generate();
|
||||
|
||||
var result = article.updateSalesUnitPrice(nonExistentId, Money.euro(new BigDecimal("5.00")));
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.SalesUnitNotFound.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when new price is zero")
|
||||
void shouldFailWhenPriceZero() {
|
||||
var article = createValidArticle();
|
||||
var salesUnitId = article.salesUnits().getFirst().id();
|
||||
|
||||
var result = article.updateSalesUnitPrice(salesUnitId, Money.zero());
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.InvalidPrice.class);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Supplier References ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("assignSupplier / removeSupplier")
|
||||
class SupplierReferences {
|
||||
|
||||
@Test
|
||||
@DisplayName("should add supplier reference")
|
||||
void shouldAddSupplierReference() {
|
||||
var article = createValidArticle();
|
||||
var supplierId = SupplierId.generate();
|
||||
|
||||
article.addSupplierReference(supplierId);
|
||||
|
||||
assertThat(article.supplierReferences()).containsExactly(supplierId);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should add multiple supplier references")
|
||||
void shouldAddMultipleSuppliers() {
|
||||
var article = createValidArticle();
|
||||
var supplier1 = SupplierId.generate();
|
||||
var supplier2 = SupplierId.generate();
|
||||
|
||||
article.addSupplierReference(supplier1);
|
||||
article.addSupplierReference(supplier2);
|
||||
|
||||
assertThat(article.supplierReferences()).containsExactlyInAnyOrder(supplier1, supplier2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not duplicate supplier reference")
|
||||
void shouldNotDuplicateSupplier() {
|
||||
var article = createValidArticle();
|
||||
var supplierId = SupplierId.generate();
|
||||
|
||||
article.addSupplierReference(supplierId);
|
||||
article.addSupplierReference(supplierId);
|
||||
|
||||
assertThat(article.supplierReferences()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should remove supplier reference")
|
||||
void shouldRemoveSupplierReference() {
|
||||
var article = createValidArticle();
|
||||
var supplierId = SupplierId.generate();
|
||||
article.addSupplierReference(supplierId);
|
||||
|
||||
article.removeSupplierReference(supplierId);
|
||||
|
||||
assertThat(article.supplierReferences()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should silently ignore removing non-existent supplier")
|
||||
void shouldIgnoreRemovingNonExistentSupplier() {
|
||||
var article = createValidArticle();
|
||||
var supplierId = SupplierId.generate();
|
||||
|
||||
article.removeSupplierReference(supplierId);
|
||||
|
||||
assertThat(article.supplierReferences()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update updatedAt on add supplier")
|
||||
void shouldUpdateTimestampOnAddSupplier() {
|
||||
var article = createValidArticle();
|
||||
var originalUpdatedAt = article.updatedAt();
|
||||
|
||||
article.addSupplierReference(SupplierId.generate());
|
||||
|
||||
assertThat(article.updatedAt()).isAfterOrEqualTo(originalUpdatedAt);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update updatedAt on remove supplier")
|
||||
void shouldUpdateTimestampOnRemoveSupplier() {
|
||||
var article = createValidArticle();
|
||||
var supplierId = SupplierId.generate();
|
||||
article.addSupplierReference(supplierId);
|
||||
var afterAdd = article.updatedAt();
|
||||
|
||||
article.removeSupplierReference(supplierId);
|
||||
|
||||
assertThat(article.updatedAt()).isAfterOrEqualTo(afterAdd);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Equality ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("equals / hashCode")
|
||||
class Equality {
|
||||
|
||||
@Test
|
||||
@DisplayName("should be equal if same ID")
|
||||
void shouldBeEqualBySameId() {
|
||||
var id = ArticleId.generate();
|
||||
var name = new ArticleName("Testwurst");
|
||||
var number = new ArticleNumber("ART-001");
|
||||
var categoryId = ProductCategoryId.generate();
|
||||
var salesUnit = SalesUnit.reconstitute(
|
||||
SalesUnitId.generate(), Unit.PIECE_FIXED, PriceModel.FIXED, Money.euro(new BigDecimal("9.99")));
|
||||
var now = java.time.OffsetDateTime.now();
|
||||
|
||||
var article1 = Article.reconstitute(id, name, number, categoryId, List.of(salesUnit),
|
||||
ArticleStatus.ACTIVE, Set.of(), now, now);
|
||||
var article2 = Article.reconstitute(id, new ArticleName("Anderer Name"), new ArticleNumber("ART-999"),
|
||||
ProductCategoryId.generate(), List.of(salesUnit), ArticleStatus.INACTIVE, Set.of(), now, now);
|
||||
|
||||
assertThat(article1).isEqualTo(article2);
|
||||
assertThat(article1.hashCode()).isEqualTo(article2.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not be equal if different ID")
|
||||
void shouldNotBeEqualByDifferentId() {
|
||||
var article1 = createValidArticle();
|
||||
var article2 = createValidArticle();
|
||||
|
||||
assertThat(article1).isNotEqualTo(article2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not be equal to null")
|
||||
void shouldNotBeEqualToNull() {
|
||||
var article = createValidArticle();
|
||||
|
||||
assertThat(article).isNotEqualTo(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not be equal to different type")
|
||||
void shouldNotBeEqualToDifferentType() {
|
||||
var article = createValidArticle();
|
||||
|
||||
assertThat(article).isNotEqualTo("not an article");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should be equal to itself")
|
||||
void shouldBeEqualToItself() {
|
||||
var article = createValidArticle();
|
||||
|
||||
assertThat(article).isEqualTo(article);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Helpers ====================
|
||||
|
||||
private static ArticleDraft validDraft() {
|
||||
return new ArticleDraft(
|
||||
"Testwurst",
|
||||
"ART-001",
|
||||
UUID.randomUUID().toString(),
|
||||
Unit.PIECE_FIXED,
|
||||
PriceModel.FIXED,
|
||||
new BigDecimal("9.99")
|
||||
);
|
||||
}
|
||||
|
||||
private static Article createValidArticle() {
|
||||
return Article.create(validDraft()).unsafeGetValue();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,858 @@
|
|||
package de.effigenix.domain.masterdata;
|
||||
|
||||
import de.effigenix.shared.common.Address;
|
||||
import de.effigenix.shared.common.Money;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class CustomerTest {
|
||||
|
||||
// ==================== Create ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("create()")
|
||||
class Create {
|
||||
|
||||
@Nested
|
||||
@DisplayName("B2C")
|
||||
class B2C {
|
||||
|
||||
@Test
|
||||
@DisplayName("should create B2C customer with valid data")
|
||||
void shouldCreateWithValidData() {
|
||||
var draft = new CustomerDraft(
|
||||
"Max Mustermann", CustomerType.B2C,
|
||||
"+49 123 456", "max@example.com", "Max",
|
||||
"Hauptstr.", "10", "12345", "Berlin", "DE",
|
||||
null, null
|
||||
);
|
||||
|
||||
var result = Customer.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var customer = result.unsafeGetValue();
|
||||
assertThat(customer.id()).isNotNull();
|
||||
assertThat(customer.name().value()).isEqualTo("Max Mustermann");
|
||||
assertThat(customer.type()).isEqualTo(CustomerType.B2C);
|
||||
assertThat(customer.billingAddress().street()).isEqualTo("Hauptstr.");
|
||||
assertThat(customer.billingAddress().houseNumber()).isEqualTo("10");
|
||||
assertThat(customer.billingAddress().postalCode()).isEqualTo("12345");
|
||||
assertThat(customer.billingAddress().city()).isEqualTo("Berlin");
|
||||
assertThat(customer.billingAddress().country()).isEqualTo("DE");
|
||||
assertThat(customer.contactInfo().phone()).isEqualTo("+49 123 456");
|
||||
assertThat(customer.contactInfo().email()).isEqualTo("max@example.com");
|
||||
assertThat(customer.contactInfo().contactPerson()).isEqualTo("Max");
|
||||
assertThat(customer.paymentTerms()).isNull();
|
||||
assertThat(customer.deliveryAddresses()).isEmpty();
|
||||
assertThat(customer.frameContract()).isNull();
|
||||
assertThat(customer.preferences()).isEmpty();
|
||||
assertThat(customer.status()).isEqualTo(CustomerStatus.ACTIVE);
|
||||
assertThat(customer.createdAt()).isNotNull();
|
||||
assertThat(customer.updatedAt()).isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("B2B")
|
||||
class B2B {
|
||||
|
||||
@Test
|
||||
@DisplayName("should create B2B customer with valid data")
|
||||
void shouldCreateWithValidData() {
|
||||
var draft = new CustomerDraft(
|
||||
"Firma GmbH", CustomerType.B2B,
|
||||
"+49 800 123", "info@firma.de", "Herr Schmidt",
|
||||
"Industriestr.", "5a", "80331", "Muenchen", "DE",
|
||||
30, "30 Tage netto"
|
||||
);
|
||||
|
||||
var result = Customer.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var customer = result.unsafeGetValue();
|
||||
assertThat(customer.name().value()).isEqualTo("Firma GmbH");
|
||||
assertThat(customer.type()).isEqualTo(CustomerType.B2B);
|
||||
assertThat(customer.paymentTerms()).isNotNull();
|
||||
assertThat(customer.paymentTerms().paymentDueDays()).isEqualTo(30);
|
||||
assertThat(customer.paymentTerms().description()).isEqualTo("30 Tage netto");
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("validation failures")
|
||||
class ValidationFailures {
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when name is null")
|
||||
void shouldFailWhenNameNull() {
|
||||
var draft = new CustomerDraft(
|
||||
null, CustomerType.B2C,
|
||||
"+49 123", null, null,
|
||||
"Str.", null, "12345", "Berlin", "DE",
|
||||
null, null
|
||||
);
|
||||
|
||||
var result = Customer.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when name is blank")
|
||||
void shouldFailWhenNameBlank() {
|
||||
var draft = new CustomerDraft(
|
||||
" ", CustomerType.B2C,
|
||||
"+49 123", null, null,
|
||||
"Str.", null, "12345", "Berlin", "DE",
|
||||
null, null
|
||||
);
|
||||
|
||||
var result = Customer.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when name exceeds 200 characters")
|
||||
void shouldFailWhenNameTooLong() {
|
||||
var draft = new CustomerDraft(
|
||||
"A".repeat(201), CustomerType.B2C,
|
||||
"+49 123", null, null,
|
||||
"Str.", null, "12345", "Berlin", "DE",
|
||||
null, null
|
||||
);
|
||||
|
||||
var result = Customer.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should accept name with exactly 200 characters")
|
||||
void shouldAcceptNameWith200Chars() {
|
||||
var draft = new CustomerDraft(
|
||||
"A".repeat(200), CustomerType.B2C,
|
||||
"+49 123", null, null,
|
||||
"Str.", null, "12345", "Berlin", "DE",
|
||||
null, null
|
||||
);
|
||||
|
||||
var result = Customer.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when email format is invalid")
|
||||
void shouldFailWhenEmailInvalid() {
|
||||
var draft = new CustomerDraft(
|
||||
"Test", CustomerType.B2C,
|
||||
"+49 123", "not-an-email", null,
|
||||
"Str.", null, "12345", "Berlin", "DE",
|
||||
null, null
|
||||
);
|
||||
|
||||
var result = Customer.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when street is blank")
|
||||
void shouldFailWhenStreetBlank() {
|
||||
var draft = new CustomerDraft(
|
||||
"Test", CustomerType.B2C,
|
||||
"+49 123", null, null,
|
||||
"", null, "12345", "Berlin", "DE",
|
||||
null, null
|
||||
);
|
||||
|
||||
var result = Customer.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when city is blank")
|
||||
void shouldFailWhenCityBlank() {
|
||||
var draft = new CustomerDraft(
|
||||
"Test", CustomerType.B2C,
|
||||
"+49 123", null, null,
|
||||
"Str.", null, "12345", "", "DE",
|
||||
null, null
|
||||
);
|
||||
|
||||
var result = Customer.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when country is not ISO alpha-2")
|
||||
void shouldFailWhenCountryInvalid() {
|
||||
var draft = new CustomerDraft(
|
||||
"Test", CustomerType.B2C,
|
||||
"+49 123", null, null,
|
||||
"Str.", null, "12345", "Berlin", "Deutschland",
|
||||
null, null
|
||||
);
|
||||
|
||||
var result = Customer.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when postalCode is blank")
|
||||
void shouldFailWhenPostalCodeBlank() {
|
||||
var draft = new CustomerDraft(
|
||||
"Test", CustomerType.B2C,
|
||||
"+49 123", null, null,
|
||||
"Str.", null, "", "Berlin", "DE",
|
||||
null, null
|
||||
);
|
||||
|
||||
var result = Customer.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when paymentDueDays is negative")
|
||||
void shouldFailWhenPaymentDueDaysNegative() {
|
||||
var draft = new CustomerDraft(
|
||||
"Test", CustomerType.B2C,
|
||||
"+49 123", null, null,
|
||||
"Str.", null, "12345", "Berlin", "DE",
|
||||
-1, null
|
||||
);
|
||||
|
||||
var result = Customer.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Update ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("update()")
|
||||
class Update {
|
||||
|
||||
@Test
|
||||
@DisplayName("should update name")
|
||||
void shouldUpdateName() {
|
||||
var customer = createValidCustomer();
|
||||
var draft = new CustomerUpdateDraft(
|
||||
"Neuer Name", null, null, null,
|
||||
null, null, null, null, null,
|
||||
null, null
|
||||
);
|
||||
|
||||
var result = customer.update(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(customer.name().value()).isEqualTo("Neuer Name");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update contactInfo")
|
||||
void shouldUpdateContactInfo() {
|
||||
var customer = createValidCustomer();
|
||||
var draft = new CustomerUpdateDraft(
|
||||
null, "+49 999 000", "new@example.com", "Frau Mueller",
|
||||
null, null, null, null, null,
|
||||
null, null
|
||||
);
|
||||
|
||||
var result = customer.update(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(customer.contactInfo().phone()).isEqualTo("+49 999 000");
|
||||
assertThat(customer.contactInfo().email()).isEqualTo("new@example.com");
|
||||
assertThat(customer.contactInfo().contactPerson()).isEqualTo("Frau Mueller");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update billingAddress")
|
||||
void shouldUpdateBillingAddress() {
|
||||
var customer = createValidCustomer();
|
||||
var draft = new CustomerUpdateDraft(
|
||||
null, null, null, null,
|
||||
"Neue Str.", "20", "54321", "Hamburg", "DE",
|
||||
null, null
|
||||
);
|
||||
|
||||
var result = customer.update(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(customer.billingAddress().street()).isEqualTo("Neue Str.");
|
||||
assertThat(customer.billingAddress().city()).isEqualTo("Hamburg");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update paymentTerms")
|
||||
void shouldUpdatePaymentTerms() {
|
||||
var customer = createValidCustomer();
|
||||
var draft = new CustomerUpdateDraft(
|
||||
null, null, null, null,
|
||||
null, null, null, null, null,
|
||||
60, "60 Tage netto"
|
||||
);
|
||||
|
||||
var result = customer.update(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(customer.paymentTerms()).isNotNull();
|
||||
assertThat(customer.paymentTerms().paymentDueDays()).isEqualTo(60);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not change fields when all null in draft")
|
||||
void shouldNotChangeFieldsWhenAllNull() {
|
||||
var customer = createValidCustomer();
|
||||
var originalName = customer.name().value();
|
||||
var originalPhone = customer.contactInfo().phone();
|
||||
var draft = new CustomerUpdateDraft(
|
||||
null, null, null, null,
|
||||
null, null, null, null, null,
|
||||
null, null
|
||||
);
|
||||
|
||||
var result = customer.update(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(customer.name().value()).isEqualTo(originalName);
|
||||
assertThat(customer.contactInfo().phone()).isEqualTo(originalPhone);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail update when name is blank")
|
||||
void shouldFailWhenNameBlank() {
|
||||
var customer = createValidCustomer();
|
||||
var draft = new CustomerUpdateDraft(
|
||||
" ", null, null, null,
|
||||
null, null, null, null, null,
|
||||
null, null
|
||||
);
|
||||
|
||||
var result = customer.update(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail update when address city is blank")
|
||||
void shouldFailWhenAddressCityBlank() {
|
||||
var customer = createValidCustomer();
|
||||
var draft = new CustomerUpdateDraft(
|
||||
null, null, null, null,
|
||||
"Str.", null, "12345", "", "DE",
|
||||
null, null
|
||||
);
|
||||
|
||||
var result = customer.update(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update updatedAt timestamp")
|
||||
void shouldUpdateTimestamp() {
|
||||
var customer = createValidCustomer();
|
||||
var beforeUpdate = customer.updatedAt();
|
||||
|
||||
var draft = new CustomerUpdateDraft(
|
||||
"Anderer Name", null, null, null,
|
||||
null, null, null, null, null,
|
||||
null, null
|
||||
);
|
||||
customer.update(draft);
|
||||
|
||||
assertThat(customer.updatedAt()).isAfterOrEqualTo(beforeUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Activate / Deactivate ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("activate() / deactivate()")
|
||||
class ActivateDeactivate {
|
||||
|
||||
@Test
|
||||
@DisplayName("should deactivate active customer")
|
||||
void shouldDeactivate() {
|
||||
var customer = createValidCustomer();
|
||||
assertThat(customer.status()).isEqualTo(CustomerStatus.ACTIVE);
|
||||
|
||||
customer.deactivate();
|
||||
|
||||
assertThat(customer.status()).isEqualTo(CustomerStatus.INACTIVE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should activate inactive customer")
|
||||
void shouldActivate() {
|
||||
var customer = createValidCustomer();
|
||||
customer.deactivate();
|
||||
assertThat(customer.status()).isEqualTo(CustomerStatus.INACTIVE);
|
||||
|
||||
customer.activate();
|
||||
|
||||
assertThat(customer.status()).isEqualTo(CustomerStatus.ACTIVE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("deactivate should update updatedAt")
|
||||
void deactivateShouldUpdateTimestamp() {
|
||||
var customer = createValidCustomer();
|
||||
var before = customer.updatedAt();
|
||||
|
||||
customer.deactivate();
|
||||
|
||||
assertThat(customer.updatedAt()).isAfterOrEqualTo(before);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("activate should update updatedAt")
|
||||
void activateShouldUpdateTimestamp() {
|
||||
var customer = createValidCustomer();
|
||||
customer.deactivate();
|
||||
var before = customer.updatedAt();
|
||||
|
||||
customer.activate();
|
||||
|
||||
assertThat(customer.updatedAt()).isAfterOrEqualTo(before);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Delivery Addresses ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("addDeliveryAddress() / removeDeliveryAddress()")
|
||||
class DeliveryAddresses {
|
||||
|
||||
@Test
|
||||
@DisplayName("should add delivery address")
|
||||
void shouldAddDeliveryAddress() {
|
||||
var customer = createValidCustomer();
|
||||
var address = Address.create("Lieferstr.", "1", "99999", "Leipzig", "DE").unsafeGetValue();
|
||||
var deliveryAddress = new DeliveryAddress("Lager Ost", address, "Herr Meier", "Rampe 3");
|
||||
|
||||
customer.addDeliveryAddress(deliveryAddress);
|
||||
|
||||
assertThat(customer.deliveryAddresses()).hasSize(1);
|
||||
assertThat(customer.deliveryAddresses().getFirst().label()).isEqualTo("Lager Ost");
|
||||
assertThat(customer.deliveryAddresses().getFirst().address().city()).isEqualTo("Leipzig");
|
||||
assertThat(customer.deliveryAddresses().getFirst().contactPerson()).isEqualTo("Herr Meier");
|
||||
assertThat(customer.deliveryAddresses().getFirst().deliveryNotes()).isEqualTo("Rampe 3");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should add multiple delivery addresses")
|
||||
void shouldAddMultipleDeliveryAddresses() {
|
||||
var customer = createValidCustomer();
|
||||
var address1 = Address.create("Str. 1", null, "11111", "Stadt A", "DE").unsafeGetValue();
|
||||
var address2 = Address.create("Str. 2", null, "22222", "Stadt B", "DE").unsafeGetValue();
|
||||
|
||||
customer.addDeliveryAddress(new DeliveryAddress("Lager 1", address1, null, null));
|
||||
customer.addDeliveryAddress(new DeliveryAddress("Lager 2", address2, null, null));
|
||||
|
||||
assertThat(customer.deliveryAddresses()).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should remove delivery address by label")
|
||||
void shouldRemoveDeliveryAddressByLabel() {
|
||||
var customer = createValidCustomer();
|
||||
var address = Address.create("Lieferstr.", "1", "99999", "Leipzig", "DE").unsafeGetValue();
|
||||
customer.addDeliveryAddress(new DeliveryAddress("Lager Ost", address, null, null));
|
||||
customer.addDeliveryAddress(new DeliveryAddress("Lager West", address, null, null));
|
||||
|
||||
customer.removeDeliveryAddress("Lager Ost");
|
||||
|
||||
assertThat(customer.deliveryAddresses()).hasSize(1);
|
||||
assertThat(customer.deliveryAddresses().getFirst().label()).isEqualTo("Lager West");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should do nothing when removing non-existent label")
|
||||
void shouldDoNothingWhenRemovingNonExistent() {
|
||||
var customer = createValidCustomer();
|
||||
var address = Address.create("Lieferstr.", "1", "99999", "Leipzig", "DE").unsafeGetValue();
|
||||
customer.addDeliveryAddress(new DeliveryAddress("Lager Ost", address, null, null));
|
||||
|
||||
customer.removeDeliveryAddress("Nicht vorhanden");
|
||||
|
||||
assertThat(customer.deliveryAddresses()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("addDeliveryAddress should update updatedAt")
|
||||
void addShouldUpdateTimestamp() {
|
||||
var customer = createValidCustomer();
|
||||
var before = customer.updatedAt();
|
||||
var address = Address.create("Str.", null, "12345", "City", "DE").unsafeGetValue();
|
||||
|
||||
customer.addDeliveryAddress(new DeliveryAddress("L", address, null, null));
|
||||
|
||||
assertThat(customer.updatedAt()).isAfterOrEqualTo(before);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("removeDeliveryAddress should update updatedAt")
|
||||
void removeShouldUpdateTimestamp() {
|
||||
var customer = createValidCustomer();
|
||||
var address = Address.create("Str.", null, "12345", "City", "DE").unsafeGetValue();
|
||||
customer.addDeliveryAddress(new DeliveryAddress("L", address, null, null));
|
||||
var before = customer.updatedAt();
|
||||
|
||||
customer.removeDeliveryAddress("L");
|
||||
|
||||
assertThat(customer.updatedAt()).isAfterOrEqualTo(before);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("deliveryAddresses should return unmodifiable list")
|
||||
void shouldReturnUnmodifiableList() {
|
||||
var customer = createValidCustomer();
|
||||
|
||||
var list = customer.deliveryAddresses();
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertThrows(
|
||||
UnsupportedOperationException.class,
|
||||
() -> list.add(null)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Frame Contract ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("setFrameContract()")
|
||||
class SetFrameContract {
|
||||
|
||||
@Test
|
||||
@DisplayName("should set frame contract for B2B customer")
|
||||
void shouldSetFrameContractForB2B() {
|
||||
var customer = createValidB2BCustomer();
|
||||
var contract = createValidFrameContract();
|
||||
|
||||
var result = customer.setFrameContract(contract);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(customer.frameContract()).isNotNull();
|
||||
assertThat(customer.frameContract()).isEqualTo(contract);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail setting frame contract for B2C customer")
|
||||
void shouldFailForB2C() {
|
||||
var customer = createValidCustomer();
|
||||
var contract = createValidFrameContract();
|
||||
|
||||
var result = customer.setFrameContract(contract);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.FrameContractNotAllowed.class);
|
||||
assertThat(customer.frameContract()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should remove frame contract")
|
||||
void shouldRemoveFrameContract() {
|
||||
var customer = createValidB2BCustomer();
|
||||
var contract = createValidFrameContract();
|
||||
customer.setFrameContract(contract);
|
||||
|
||||
customer.removeFrameContract();
|
||||
|
||||
assertThat(customer.frameContract()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("hasActiveFrameContract should return true for active contract")
|
||||
void shouldReturnTrueForActiveContract() {
|
||||
var customer = createValidB2BCustomer();
|
||||
var lineItem = new ContractLineItem(
|
||||
new ArticleId("ART-001"), Money.euro(new BigDecimal("10.00")),
|
||||
new BigDecimal("100"), Unit.KG
|
||||
);
|
||||
var contract = FrameContract.create(
|
||||
LocalDate.now().minusDays(1), LocalDate.now().plusDays(30),
|
||||
DeliveryRhythm.WEEKLY, List.of(lineItem)
|
||||
).unsafeGetValue();
|
||||
customer.setFrameContract(contract);
|
||||
|
||||
assertThat(customer.hasActiveFrameContract()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("hasActiveFrameContract should return false when no contract")
|
||||
void shouldReturnFalseWhenNoContract() {
|
||||
var customer = createValidB2BCustomer();
|
||||
|
||||
assertThat(customer.hasActiveFrameContract()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getAgreedPrice should return price for existing article")
|
||||
void shouldReturnAgreedPrice() {
|
||||
var customer = createValidB2BCustomer();
|
||||
var articleId = new ArticleId("ART-001");
|
||||
var price = Money.euro(new BigDecimal("12.50"));
|
||||
var lineItem = new ContractLineItem(articleId, price, new BigDecimal("100"), Unit.KG);
|
||||
var contract = FrameContract.create(
|
||||
LocalDate.now().minusDays(1), LocalDate.now().plusDays(30),
|
||||
DeliveryRhythm.WEEKLY, List.of(lineItem)
|
||||
).unsafeGetValue();
|
||||
customer.setFrameContract(contract);
|
||||
|
||||
var result = customer.getAgreedPrice(articleId);
|
||||
|
||||
assertThat(result).isPresent();
|
||||
assertThat(result.get()).isEqualTo(price);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getAgreedPrice should return empty for unknown article")
|
||||
void shouldReturnEmptyForUnknownArticle() {
|
||||
var customer = createValidB2BCustomer();
|
||||
var lineItem = new ContractLineItem(
|
||||
new ArticleId("ART-001"), Money.euro(new BigDecimal("10.00")),
|
||||
new BigDecimal("100"), Unit.KG
|
||||
);
|
||||
var contract = FrameContract.create(
|
||||
LocalDate.now().minusDays(1), LocalDate.now().plusDays(30),
|
||||
DeliveryRhythm.WEEKLY, List.of(lineItem)
|
||||
).unsafeGetValue();
|
||||
customer.setFrameContract(contract);
|
||||
|
||||
var result = customer.getAgreedPrice(new ArticleId("ART-999"));
|
||||
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getAgreedPrice should return empty when no contract set")
|
||||
void shouldReturnEmptyWhenNoContract() {
|
||||
var customer = createValidB2BCustomer();
|
||||
|
||||
var result = customer.getAgreedPrice(new ArticleId("ART-001"));
|
||||
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("setFrameContract should update updatedAt")
|
||||
void shouldUpdateTimestamp() {
|
||||
var customer = createValidB2BCustomer();
|
||||
var before = customer.updatedAt();
|
||||
var contract = createValidFrameContract();
|
||||
|
||||
customer.setFrameContract(contract);
|
||||
|
||||
assertThat(customer.updatedAt()).isAfterOrEqualTo(before);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Preferences ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("setPreferences()")
|
||||
class Preferences {
|
||||
|
||||
@Test
|
||||
@DisplayName("should set preferences")
|
||||
void shouldSetPreferences() {
|
||||
var customer = createValidCustomer();
|
||||
|
||||
customer.setPreferences(Set.of(CustomerPreference.BIO, CustomerPreference.REGIONAL));
|
||||
|
||||
assertThat(customer.preferences()).containsExactlyInAnyOrder(
|
||||
CustomerPreference.BIO, CustomerPreference.REGIONAL
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should replace existing preferences")
|
||||
void shouldReplaceExistingPreferences() {
|
||||
var customer = createValidCustomer();
|
||||
customer.setPreferences(Set.of(CustomerPreference.BIO, CustomerPreference.HALAL));
|
||||
|
||||
customer.setPreferences(Set.of(CustomerPreference.KOSHER));
|
||||
|
||||
assertThat(customer.preferences()).containsExactly(CustomerPreference.KOSHER);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should clear preferences with empty set")
|
||||
void shouldClearPreferencesWithEmptySet() {
|
||||
var customer = createValidCustomer();
|
||||
customer.setPreferences(Set.of(CustomerPreference.BIO));
|
||||
|
||||
customer.setPreferences(Set.of());
|
||||
|
||||
assertThat(customer.preferences()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should add single preference")
|
||||
void shouldAddSinglePreference() {
|
||||
var customer = createValidCustomer();
|
||||
|
||||
customer.addPreference(CustomerPreference.BIO);
|
||||
|
||||
assertThat(customer.preferences()).contains(CustomerPreference.BIO);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should remove single preference")
|
||||
void shouldRemoveSinglePreference() {
|
||||
var customer = createValidCustomer();
|
||||
customer.addPreference(CustomerPreference.BIO);
|
||||
customer.addPreference(CustomerPreference.REGIONAL);
|
||||
|
||||
customer.removePreference(CustomerPreference.BIO);
|
||||
|
||||
assertThat(customer.preferences()).containsExactly(CustomerPreference.REGIONAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("setPreferences should update updatedAt")
|
||||
void shouldUpdateTimestamp() {
|
||||
var customer = createValidCustomer();
|
||||
var before = customer.updatedAt();
|
||||
|
||||
customer.setPreferences(Set.of(CustomerPreference.TIERWOHL));
|
||||
|
||||
assertThat(customer.updatedAt()).isAfterOrEqualTo(before);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("preferences should return unmodifiable set")
|
||||
void shouldReturnUnmodifiableSet() {
|
||||
var customer = createValidCustomer();
|
||||
|
||||
var prefs = customer.preferences();
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertThrows(
|
||||
UnsupportedOperationException.class,
|
||||
() -> prefs.add(CustomerPreference.BIO)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Equality ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("equals / hashCode")
|
||||
class Equality {
|
||||
|
||||
@Test
|
||||
@DisplayName("should be equal if same ID")
|
||||
void shouldBeEqualBySameId() {
|
||||
var id = CustomerId.generate();
|
||||
var name = new CustomerName("Test");
|
||||
var address = Address.create("Str.", null, "12345", "City", "DE").unsafeGetValue();
|
||||
var contact = new de.effigenix.shared.common.ContactInfo("+49 1", null, null);
|
||||
var now = java.time.OffsetDateTime.now();
|
||||
|
||||
var c1 = Customer.reconstitute(id, name, CustomerType.B2C, address, contact,
|
||||
null, List.of(), null, Set.of(), CustomerStatus.ACTIVE, now, now);
|
||||
var c2 = Customer.reconstitute(id, new CustomerName("Anderer Name"), CustomerType.B2B, address, contact,
|
||||
null, List.of(), null, Set.of(), CustomerStatus.INACTIVE, now, now);
|
||||
|
||||
assertThat(c1).isEqualTo(c2);
|
||||
assertThat(c1.hashCode()).isEqualTo(c2.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not be equal if different ID")
|
||||
void shouldNotBeEqualByDifferentId() {
|
||||
var c1 = createValidCustomer();
|
||||
var c2 = createValidCustomer();
|
||||
|
||||
assertThat(c1).isNotEqualTo(c2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not be equal to null")
|
||||
void shouldNotBeEqualToNull() {
|
||||
var customer = createValidCustomer();
|
||||
|
||||
assertThat(customer).isNotEqualTo(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not be equal to different type")
|
||||
void shouldNotBeEqualToDifferentType() {
|
||||
var customer = createValidCustomer();
|
||||
|
||||
assertThat(customer).isNotEqualTo("not a customer");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should be equal to itself")
|
||||
void shouldBeEqualToItself() {
|
||||
var customer = createValidCustomer();
|
||||
|
||||
assertThat(customer).isEqualTo(customer);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Helpers ====================
|
||||
|
||||
private Customer createValidCustomer() {
|
||||
var draft = new CustomerDraft(
|
||||
"Testkunde B2C", CustomerType.B2C,
|
||||
"+49 123 456", "test@example.com", null,
|
||||
"Teststr.", "1", "12345", "Berlin", "DE",
|
||||
null, null
|
||||
);
|
||||
return Customer.create(draft).unsafeGetValue();
|
||||
}
|
||||
|
||||
private Customer createValidB2BCustomer() {
|
||||
var draft = new CustomerDraft(
|
||||
"Testkunde B2B GmbH", CustomerType.B2B,
|
||||
"+49 800 999", "b2b@example.com", "Herr Test",
|
||||
"Industriestr.", "42", "80331", "Muenchen", "DE",
|
||||
30, "30 Tage netto"
|
||||
);
|
||||
return Customer.create(draft).unsafeGetValue();
|
||||
}
|
||||
|
||||
private FrameContract createValidFrameContract() {
|
||||
var lineItem = new ContractLineItem(
|
||||
new ArticleId("ART-001"),
|
||||
Money.euro(new BigDecimal("10.00")),
|
||||
new BigDecimal("100"),
|
||||
Unit.KG
|
||||
);
|
||||
return FrameContract.create(
|
||||
LocalDate.now().minusDays(1), LocalDate.now().plusDays(30),
|
||||
DeliveryRhythm.WEEKLY, List.of(lineItem)
|
||||
).unsafeGetValue();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
package de.effigenix.domain.masterdata;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ProductCategoryTest {
|
||||
|
||||
// ==================== Create ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("create()")
|
||||
class Create {
|
||||
|
||||
@Test
|
||||
@DisplayName("should create ProductCategory with valid data")
|
||||
void shouldCreateWithValidData() {
|
||||
var draft = new ProductCategoryDraft("Backwaren", "Alle Backwaren-Produkte");
|
||||
|
||||
var result = ProductCategory.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var category = result.unsafeGetValue();
|
||||
assertThat(category.id()).isNotNull();
|
||||
assertThat(category.name().value()).isEqualTo("Backwaren");
|
||||
assertThat(category.description()).isEqualTo("Alle Backwaren-Produkte");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should create ProductCategory without description")
|
||||
void shouldCreateWithoutDescription() {
|
||||
var draft = new ProductCategoryDraft("Backwaren", null);
|
||||
|
||||
var result = ProductCategory.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var category = result.unsafeGetValue();
|
||||
assertThat(category.name().value()).isEqualTo("Backwaren");
|
||||
assertThat(category.description()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when name is null")
|
||||
void shouldFailWhenNameNull() {
|
||||
var draft = new ProductCategoryDraft(null, "Beschreibung");
|
||||
|
||||
var result = ProductCategory.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when name is blank")
|
||||
void shouldFailWhenNameBlank() {
|
||||
var draft = new ProductCategoryDraft(" ", "Beschreibung");
|
||||
|
||||
var result = ProductCategory.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when name is empty")
|
||||
void shouldFailWhenNameEmpty() {
|
||||
var draft = new ProductCategoryDraft("", "Beschreibung");
|
||||
|
||||
var result = ProductCategory.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when name exceeds 100 characters")
|
||||
void shouldFailWhenNameTooLong() {
|
||||
var longName = "A".repeat(101);
|
||||
var draft = new ProductCategoryDraft(longName, null);
|
||||
|
||||
var result = ProductCategory.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should accept name with exactly 100 characters")
|
||||
void shouldAcceptNameWith100Chars() {
|
||||
var name = "A".repeat(100);
|
||||
var draft = new ProductCategoryDraft(name, null);
|
||||
|
||||
var result = ProductCategory.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().name().value()).isEqualTo(name);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should generate unique IDs for different categories")
|
||||
void shouldGenerateUniqueIds() {
|
||||
var cat1 = ProductCategory.create(new ProductCategoryDraft("Kategorie 1", null)).unsafeGetValue();
|
||||
var cat2 = ProductCategory.create(new ProductCategoryDraft("Kategorie 2", null)).unsafeGetValue();
|
||||
|
||||
assertThat(cat1.id()).isNotEqualTo(cat2.id());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Update ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("update()")
|
||||
class Update {
|
||||
|
||||
@Test
|
||||
@DisplayName("should update name")
|
||||
void shouldUpdateName() {
|
||||
var category = createValidCategory();
|
||||
var updateDraft = new ProductCategoryUpdateDraft("Neuer Name", null);
|
||||
|
||||
var result = category.update(updateDraft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(category.name().value()).isEqualTo("Neuer Name");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update description")
|
||||
void shouldUpdateDescription() {
|
||||
var category = createValidCategory();
|
||||
var updateDraft = new ProductCategoryUpdateDraft(null, "Neue Beschreibung");
|
||||
|
||||
var result = category.update(updateDraft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(category.description()).isEqualTo("Neue Beschreibung");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update name and description together")
|
||||
void shouldUpdateNameAndDescription() {
|
||||
var category = createValidCategory();
|
||||
var updateDraft = new ProductCategoryUpdateDraft("Neuer Name", "Neue Beschreibung");
|
||||
|
||||
var result = category.update(updateDraft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(category.name().value()).isEqualTo("Neuer Name");
|
||||
assertThat(category.description()).isEqualTo("Neue Beschreibung");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not change name when null in draft")
|
||||
void shouldNotChangeNameWhenNull() {
|
||||
var category = createValidCategory();
|
||||
var originalName = category.name().value();
|
||||
var updateDraft = new ProductCategoryUpdateDraft(null, null);
|
||||
|
||||
var result = category.update(updateDraft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(category.name().value()).isEqualTo(originalName);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not change description when null in draft")
|
||||
void shouldNotChangeDescriptionWhenNull() {
|
||||
var category = createValidCategory();
|
||||
var originalDescription = category.description();
|
||||
var updateDraft = new ProductCategoryUpdateDraft(null, null);
|
||||
|
||||
var result = category.update(updateDraft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(category.description()).isEqualTo(originalDescription);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail update when name is blank")
|
||||
void shouldFailUpdateWhenNameBlank() {
|
||||
var category = createValidCategory();
|
||||
var originalName = category.name().value();
|
||||
var updateDraft = new ProductCategoryUpdateDraft(" ", null);
|
||||
|
||||
var result = category.update(updateDraft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
|
||||
assertThat(category.name().value()).isEqualTo(originalName);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail update when name exceeds 100 characters")
|
||||
void shouldFailUpdateWhenNameTooLong() {
|
||||
var category = createValidCategory();
|
||||
var originalName = category.name().value();
|
||||
var updateDraft = new ProductCategoryUpdateDraft("A".repeat(101), null);
|
||||
|
||||
var result = category.update(updateDraft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.ValidationFailure.class);
|
||||
assertThat(category.name().value()).isEqualTo(originalName);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Equality ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("equals / hashCode")
|
||||
class Equality {
|
||||
|
||||
@Test
|
||||
@DisplayName("should be equal if same ID")
|
||||
void shouldBeEqualBySameId() {
|
||||
var id = ProductCategoryId.generate();
|
||||
var cat1 = ProductCategory.reconstitute(id, new CategoryName("Backwaren"), "Beschreibung A");
|
||||
var cat2 = ProductCategory.reconstitute(id, new CategoryName("Getranke"), "Beschreibung B");
|
||||
|
||||
assertThat(cat1).isEqualTo(cat2);
|
||||
assertThat(cat1.hashCode()).isEqualTo(cat2.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not be equal if different ID")
|
||||
void shouldNotBeEqualByDifferentId() {
|
||||
var cat1 = createValidCategory();
|
||||
var cat2 = createValidCategory();
|
||||
|
||||
assertThat(cat1).isNotEqualTo(cat2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should be equal to itself")
|
||||
void shouldBeEqualToItself() {
|
||||
var category = createValidCategory();
|
||||
|
||||
assertThat(category).isEqualTo(category);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not be equal to null")
|
||||
void shouldNotBeEqualToNull() {
|
||||
var category = createValidCategory();
|
||||
|
||||
assertThat(category).isNotEqualTo(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not be equal to different type")
|
||||
void shouldNotBeEqualToDifferentType() {
|
||||
var category = createValidCategory();
|
||||
|
||||
assertThat(category).isNotEqualTo("not a category");
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Helpers ====================
|
||||
|
||||
private ProductCategory createValidCategory() {
|
||||
var draft = new ProductCategoryDraft("Backwaren", "Alle Backwaren-Produkte");
|
||||
return ProductCategory.create(draft).unsafeGetValue();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,653 @@
|
|||
package de.effigenix.domain.masterdata;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class SupplierTest {
|
||||
|
||||
// ==================== Create ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("create()")
|
||||
class Create {
|
||||
|
||||
@Test
|
||||
@DisplayName("should create Supplier with mandatory fields only")
|
||||
void shouldCreateWithMandatoryFieldsOnly() {
|
||||
var draft = new SupplierDraft(
|
||||
"Lieferant A", "+49 123 456", null, null,
|
||||
null, null, null, null, null, null, null);
|
||||
|
||||
var result = Supplier.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var supplier = result.unsafeGetValue();
|
||||
assertThat(supplier.id()).isNotNull();
|
||||
assertThat(supplier.name().value()).isEqualTo("Lieferant A");
|
||||
assertThat(supplier.contactInfo().phone()).isEqualTo("+49 123 456");
|
||||
assertThat(supplier.address()).isNull();
|
||||
assertThat(supplier.paymentTerms()).isNull();
|
||||
assertThat(supplier.certificates()).isEmpty();
|
||||
assertThat(supplier.rating()).isNull();
|
||||
assertThat(supplier.status()).isEqualTo(SupplierStatus.ACTIVE);
|
||||
assertThat(supplier.createdAt()).isNotNull();
|
||||
assertThat(supplier.updatedAt()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should create Supplier with all optional fields")
|
||||
void shouldCreateWithAllOptionalFields() {
|
||||
var draft = new SupplierDraft(
|
||||
"Lieferant B", "+49 999 888", "info@lieferant.de", "Max Mustermann",
|
||||
"Hauptstr.", "42", "12345", "Berlin", "DE", 30, "Netto 30 Tage");
|
||||
|
||||
var result = Supplier.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var supplier = result.unsafeGetValue();
|
||||
assertThat(supplier.name().value()).isEqualTo("Lieferant B");
|
||||
assertThat(supplier.contactInfo().phone()).isEqualTo("+49 999 888");
|
||||
assertThat(supplier.contactInfo().email()).isEqualTo("info@lieferant.de");
|
||||
assertThat(supplier.contactInfo().contactPerson()).isEqualTo("Max Mustermann");
|
||||
assertThat(supplier.address()).isNotNull();
|
||||
assertThat(supplier.address().street()).isEqualTo("Hauptstr.");
|
||||
assertThat(supplier.address().city()).isEqualTo("Berlin");
|
||||
assertThat(supplier.address().country()).isEqualTo("DE");
|
||||
assertThat(supplier.paymentTerms()).isNotNull();
|
||||
assertThat(supplier.paymentTerms().paymentDueDays()).isEqualTo(30);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when name is null")
|
||||
void shouldFailWhenNameNull() {
|
||||
var draft = new SupplierDraft(
|
||||
null, "+49 123 456", null, null,
|
||||
null, null, null, null, null, null, null);
|
||||
|
||||
var result = Supplier.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when name is blank")
|
||||
void shouldFailWhenNameBlank() {
|
||||
var draft = new SupplierDraft(
|
||||
" ", "+49 123 456", null, null,
|
||||
null, null, null, null, null, null, null);
|
||||
|
||||
var result = Supplier.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when name exceeds 200 chars")
|
||||
void shouldFailWhenNameTooLong() {
|
||||
var longName = "A".repeat(201);
|
||||
var draft = new SupplierDraft(
|
||||
longName, "+49 123 456", null, null,
|
||||
null, null, null, null, null, null, null);
|
||||
|
||||
var result = Supplier.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should accept name with exactly 200 chars")
|
||||
void shouldAcceptNameWith200Chars() {
|
||||
var name = "A".repeat(200);
|
||||
var draft = new SupplierDraft(
|
||||
name, "+49 123 456", null, null,
|
||||
null, null, null, null, null, null, null);
|
||||
|
||||
var result = Supplier.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when email format is invalid")
|
||||
void shouldFailWhenEmailInvalid() {
|
||||
var draft = new SupplierDraft(
|
||||
"Lieferant", "+49 123 456", "invalid-email", null,
|
||||
null, null, null, null, null, null, null);
|
||||
|
||||
var result = Supplier.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when address is incomplete (street without city)")
|
||||
void shouldFailWhenAddressIncomplete() {
|
||||
var draft = new SupplierDraft(
|
||||
"Lieferant", "+49 123 456", null, null,
|
||||
"Hauptstr.", null, null, null, null, null, null);
|
||||
|
||||
var result = Supplier.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when country code is invalid")
|
||||
void shouldFailWhenCountryCodeInvalid() {
|
||||
var draft = new SupplierDraft(
|
||||
"Lieferant", "+49 123 456", null, null,
|
||||
"Hauptstr.", "1", "12345", "Berlin", "Deutschland", null, null);
|
||||
|
||||
var result = Supplier.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail when paymentDueDays is negative")
|
||||
void shouldFailWhenPaymentDueDaysNegative() {
|
||||
var draft = new SupplierDraft(
|
||||
"Lieferant", "+49 123 456", null, null,
|
||||
null, null, null, null, null, -1, null);
|
||||
|
||||
var result = Supplier.create(draft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not create address when street and city are both null")
|
||||
void shouldNotCreateAddressWhenBothNull() {
|
||||
var draft = new SupplierDraft(
|
||||
"Lieferant", "+49 123 456", null, null,
|
||||
null, null, "12345", null, "DE", null, null);
|
||||
|
||||
var result = Supplier.create(draft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().address()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should trigger address creation when city is set")
|
||||
void shouldTriggerAddressCreationWhenCitySet() {
|
||||
var draft = new SupplierDraft(
|
||||
"Lieferant", "+49 123 456", null, null,
|
||||
null, null, null, "Berlin", null, null, null);
|
||||
|
||||
var result = Supplier.create(draft);
|
||||
|
||||
// Address creation triggered but street is null -> validation failure
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Update ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("update()")
|
||||
class Update {
|
||||
|
||||
@Test
|
||||
@DisplayName("should update name")
|
||||
void shouldUpdateName() {
|
||||
var supplier = createValidSupplier();
|
||||
var updateDraft = new SupplierUpdateDraft(
|
||||
"Neuer Name", null, null, null,
|
||||
null, null, null, null, null, null, null);
|
||||
|
||||
var result = supplier.update(updateDraft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(supplier.name().value()).isEqualTo("Neuer Name");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update contact info")
|
||||
void shouldUpdateContactInfo() {
|
||||
var supplier = createValidSupplier();
|
||||
var updateDraft = new SupplierUpdateDraft(
|
||||
null, "+49 111 222", "new@mail.de", "Erika Muster",
|
||||
null, null, null, null, null, null, null);
|
||||
|
||||
var result = supplier.update(updateDraft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(supplier.contactInfo().phone()).isEqualTo("+49 111 222");
|
||||
assertThat(supplier.contactInfo().email()).isEqualTo("new@mail.de");
|
||||
assertThat(supplier.contactInfo().contactPerson()).isEqualTo("Erika Muster");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update address")
|
||||
void shouldUpdateAddress() {
|
||||
var supplier = createValidSupplier();
|
||||
var updateDraft = new SupplierUpdateDraft(
|
||||
null, null, null, null,
|
||||
"Neue Str.", "10", "54321", "Hamburg", "DE", null, null);
|
||||
|
||||
var result = supplier.update(updateDraft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(supplier.address()).isNotNull();
|
||||
assertThat(supplier.address().street()).isEqualTo("Neue Str.");
|
||||
assertThat(supplier.address().city()).isEqualTo("Hamburg");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update payment terms")
|
||||
void shouldUpdatePaymentTerms() {
|
||||
var supplier = createValidSupplier();
|
||||
var updateDraft = new SupplierUpdateDraft(
|
||||
null, null, null, null,
|
||||
null, null, null, null, null, 60, "Netto 60 Tage");
|
||||
|
||||
var result = supplier.update(updateDraft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(supplier.paymentTerms()).isNotNull();
|
||||
assertThat(supplier.paymentTerms().paymentDueDays()).isEqualTo(60);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not change fields when all null in draft")
|
||||
void shouldNotChangeFieldsWhenAllNull() {
|
||||
var supplier = createValidSupplier();
|
||||
var originalName = supplier.name().value();
|
||||
var originalPhone = supplier.contactInfo().phone();
|
||||
var updateDraft = new SupplierUpdateDraft(
|
||||
null, null, null, null,
|
||||
null, null, null, null, null, null, null);
|
||||
|
||||
var result = supplier.update(updateDraft);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(supplier.name().value()).isEqualTo(originalName);
|
||||
assertThat(supplier.contactInfo().phone()).isEqualTo(originalPhone);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail update when name is blank")
|
||||
void shouldFailUpdateWhenNameBlank() {
|
||||
var supplier = createValidSupplier();
|
||||
var updateDraft = new SupplierUpdateDraft(
|
||||
"", null, null, null,
|
||||
null, null, null, null, null, null, null);
|
||||
|
||||
var result = supplier.update(updateDraft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail update when email is invalid")
|
||||
void shouldFailUpdateWhenEmailInvalid() {
|
||||
var supplier = createValidSupplier();
|
||||
var updateDraft = new SupplierUpdateDraft(
|
||||
null, "+49 123 456", "not-an-email", null,
|
||||
null, null, null, null, null, null, null);
|
||||
|
||||
var result = supplier.update(updateDraft);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.ValidationFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update updatedAt timestamp")
|
||||
void shouldUpdateTimestamp() {
|
||||
var supplier = createValidSupplier();
|
||||
var before = supplier.updatedAt();
|
||||
|
||||
supplier.update(new SupplierUpdateDraft(
|
||||
"Geaendert", null, null, null,
|
||||
null, null, null, null, null, null, null));
|
||||
|
||||
assertThat(supplier.updatedAt()).isAfterOrEqualTo(before);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Activate / Deactivate ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("activate() / deactivate()")
|
||||
class ActivateDeactivate {
|
||||
|
||||
@Test
|
||||
@DisplayName("should deactivate active supplier")
|
||||
void shouldDeactivate() {
|
||||
var supplier = createValidSupplier();
|
||||
assertThat(supplier.status()).isEqualTo(SupplierStatus.ACTIVE);
|
||||
|
||||
supplier.deactivate();
|
||||
|
||||
assertThat(supplier.status()).isEqualTo(SupplierStatus.INACTIVE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should activate inactive supplier")
|
||||
void shouldActivate() {
|
||||
var supplier = createValidSupplier();
|
||||
supplier.deactivate();
|
||||
assertThat(supplier.status()).isEqualTo(SupplierStatus.INACTIVE);
|
||||
|
||||
supplier.activate();
|
||||
|
||||
assertThat(supplier.status()).isEqualTo(SupplierStatus.ACTIVE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update updatedAt on deactivate")
|
||||
void shouldUpdateTimestampOnDeactivate() {
|
||||
var supplier = createValidSupplier();
|
||||
var before = supplier.updatedAt();
|
||||
|
||||
supplier.deactivate();
|
||||
|
||||
assertThat(supplier.updatedAt()).isAfterOrEqualTo(before);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update updatedAt on activate")
|
||||
void shouldUpdateTimestampOnActivate() {
|
||||
var supplier = createValidSupplier();
|
||||
supplier.deactivate();
|
||||
var before = supplier.updatedAt();
|
||||
|
||||
supplier.activate();
|
||||
|
||||
assertThat(supplier.updatedAt()).isAfterOrEqualTo(before);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Rate ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("rate()")
|
||||
class Rate {
|
||||
|
||||
@Test
|
||||
@DisplayName("should set rating")
|
||||
void shouldSetRating() {
|
||||
var supplier = createValidSupplier();
|
||||
var rating = new SupplierRating(4, 3, 5);
|
||||
|
||||
supplier.rate(rating);
|
||||
|
||||
assertThat(supplier.rating()).isEqualTo(rating);
|
||||
assertThat(supplier.rating().qualityScore()).isEqualTo(4);
|
||||
assertThat(supplier.rating().deliveryScore()).isEqualTo(3);
|
||||
assertThat(supplier.rating().priceScore()).isEqualTo(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should overwrite existing rating")
|
||||
void shouldOverwriteExistingRating() {
|
||||
var supplier = createValidSupplier();
|
||||
supplier.rate(new SupplierRating(1, 1, 1));
|
||||
|
||||
var newRating = new SupplierRating(5, 5, 5);
|
||||
supplier.rate(newRating);
|
||||
|
||||
assertThat(supplier.rating()).isEqualTo(newRating);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update updatedAt on rate")
|
||||
void shouldUpdateTimestampOnRate() {
|
||||
var supplier = createValidSupplier();
|
||||
var before = supplier.updatedAt();
|
||||
|
||||
supplier.rate(new SupplierRating(3, 3, 3));
|
||||
|
||||
assertThat(supplier.updatedAt()).isAfterOrEqualTo(before);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Certificates ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("addCertificate() / removeCertificate()")
|
||||
class Certificates {
|
||||
|
||||
@Test
|
||||
@DisplayName("should add certificate")
|
||||
void shouldAddCertificate() {
|
||||
var supplier = createValidSupplier();
|
||||
var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));
|
||||
|
||||
supplier.addCertificate(cert);
|
||||
|
||||
assertThat(supplier.certificates()).hasSize(1);
|
||||
assertThat(supplier.certificates().getFirst()).isEqualTo(cert);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not add duplicate certificate")
|
||||
void shouldNotAddDuplicate() {
|
||||
var supplier = createValidSupplier();
|
||||
var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));
|
||||
|
||||
supplier.addCertificate(cert);
|
||||
supplier.addCertificate(cert);
|
||||
|
||||
assertThat(supplier.certificates()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should add different certificates")
|
||||
void shouldAddDifferentCertificates() {
|
||||
var supplier = createValidSupplier();
|
||||
var cert1 = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));
|
||||
var cert2 = new QualityCertificate("ISO 22000", "Dekra", LocalDate.of(2025, 6, 1), LocalDate.of(2027, 6, 1));
|
||||
|
||||
supplier.addCertificate(cert1);
|
||||
supplier.addCertificate(cert2);
|
||||
|
||||
assertThat(supplier.certificates()).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should remove certificate")
|
||||
void shouldRemoveCertificate() {
|
||||
var supplier = createValidSupplier();
|
||||
var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));
|
||||
supplier.addCertificate(cert);
|
||||
|
||||
supplier.removeCertificate(cert);
|
||||
|
||||
assertThat(supplier.certificates()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not fail when removing non-existent certificate")
|
||||
void shouldNotFailWhenRemovingNonExistent() {
|
||||
var supplier = createValidSupplier();
|
||||
var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));
|
||||
|
||||
supplier.removeCertificate(cert);
|
||||
|
||||
assertThat(supplier.certificates()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return unmodifiable certificate list")
|
||||
void shouldReturnUnmodifiableCertificateList() {
|
||||
var supplier = createValidSupplier();
|
||||
var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));
|
||||
supplier.addCertificate(cert);
|
||||
|
||||
var certs = supplier.certificates();
|
||||
|
||||
org.junit.jupiter.api.Assertions.assertThrows(
|
||||
UnsupportedOperationException.class,
|
||||
() -> certs.add(new QualityCertificate("HACCP", "Bureau Veritas", null, null))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update updatedAt on addCertificate")
|
||||
void shouldUpdateTimestampOnAdd() {
|
||||
var supplier = createValidSupplier();
|
||||
var before = supplier.updatedAt();
|
||||
var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));
|
||||
|
||||
supplier.addCertificate(cert);
|
||||
|
||||
assertThat(supplier.updatedAt()).isAfterOrEqualTo(before);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should update updatedAt on removeCertificate")
|
||||
void shouldUpdateTimestampOnRemove() {
|
||||
var supplier = createValidSupplier();
|
||||
var cert = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2025, 1, 1), LocalDate.of(2028, 1, 1));
|
||||
supplier.addCertificate(cert);
|
||||
var before = supplier.updatedAt();
|
||||
|
||||
supplier.removeCertificate(cert);
|
||||
|
||||
assertThat(supplier.updatedAt()).isAfterOrEqualTo(before);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Expired / Expiring Certificates ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("expiredCertificates() / certificatesExpiringSoon()")
|
||||
class CertificateQueries {
|
||||
|
||||
@Test
|
||||
@DisplayName("should return expired certificates")
|
||||
void shouldReturnExpiredCertificates() {
|
||||
var supplier = createValidSupplier();
|
||||
var expired = new QualityCertificate("ISO 9001", "TUV", LocalDate.of(2020, 1, 1), LocalDate.of(2022, 1, 1));
|
||||
var valid = new QualityCertificate("HACCP", "Dekra", LocalDate.of(2025, 1, 1), LocalDate.of(2030, 1, 1));
|
||||
supplier.addCertificate(expired);
|
||||
supplier.addCertificate(valid);
|
||||
|
||||
var result = supplier.expiredCertificates();
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.getFirst().certificateType()).isEqualTo("ISO 9001");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return empty list when no certificates are expired")
|
||||
void shouldReturnEmptyWhenNoneExpired() {
|
||||
var supplier = createValidSupplier();
|
||||
var valid = new QualityCertificate("HACCP", "Dekra", LocalDate.of(2025, 1, 1), LocalDate.of(2030, 1, 1));
|
||||
supplier.addCertificate(valid);
|
||||
|
||||
assertThat(supplier.expiredCertificates()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return certificates expiring within given days")
|
||||
void shouldReturnCertificatesExpiringSoon() {
|
||||
var supplier = createValidSupplier();
|
||||
var expiringSoon = new QualityCertificate("ISO 9001", "TUV",
|
||||
LocalDate.of(2025, 1, 1), LocalDate.now().plusDays(15));
|
||||
var farFuture = new QualityCertificate("HACCP", "Dekra",
|
||||
LocalDate.of(2025, 1, 1), LocalDate.now().plusDays(365));
|
||||
supplier.addCertificate(expiringSoon);
|
||||
supplier.addCertificate(farFuture);
|
||||
|
||||
var result = supplier.certificatesExpiringSoon(30);
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.getFirst().certificateType()).isEqualTo("ISO 9001");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not include certificates without validUntil in expiring soon")
|
||||
void shouldNotIncludeCertsWithoutValidUntil() {
|
||||
var supplier = createValidSupplier();
|
||||
var noExpiry = new QualityCertificate("Bio", "Bioland", null, null);
|
||||
supplier.addCertificate(noExpiry);
|
||||
|
||||
assertThat(supplier.certificatesExpiringSoon(30)).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Equality ====================
|
||||
|
||||
@Nested
|
||||
@DisplayName("equals / hashCode")
|
||||
class Equality {
|
||||
|
||||
@Test
|
||||
@DisplayName("should be equal if same ID")
|
||||
void shouldBeEqualBySameId() {
|
||||
var id = SupplierId.generate();
|
||||
var now = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
var name = new SupplierName("Lieferant");
|
||||
var contactInfo = new de.effigenix.shared.common.ContactInfo("+49 123", null, null);
|
||||
|
||||
var s1 = Supplier.reconstitute(id, name, null, contactInfo, null, List.of(), null, SupplierStatus.ACTIVE, now, now);
|
||||
var s2 = Supplier.reconstitute(id, new SupplierName("Anderer Name"), null, contactInfo, null, List.of(), null, SupplierStatus.INACTIVE, now, now);
|
||||
|
||||
assertThat(s1).isEqualTo(s2);
|
||||
assertThat(s1.hashCode()).isEqualTo(s2.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not be equal if different ID")
|
||||
void shouldNotBeEqualByDifferentId() {
|
||||
var s1 = createValidSupplier();
|
||||
var s2 = createValidSupplier();
|
||||
|
||||
assertThat(s1).isNotEqualTo(s2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should be equal to itself")
|
||||
void shouldBeEqualToItself() {
|
||||
var supplier = createValidSupplier();
|
||||
|
||||
assertThat(supplier).isEqualTo(supplier);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not be equal to null")
|
||||
void shouldNotBeEqualToNull() {
|
||||
var supplier = createValidSupplier();
|
||||
|
||||
assertThat(supplier).isNotEqualTo(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should not be equal to different type")
|
||||
void shouldNotBeEqualToDifferentType() {
|
||||
var supplier = createValidSupplier();
|
||||
|
||||
assertThat(supplier).isNotEqualTo("not a supplier");
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Helpers ====================
|
||||
|
||||
private Supplier createValidSupplier() {
|
||||
var draft = new SupplierDraft(
|
||||
"Test Lieferant", "+49 123 456", "test@example.com", "Max Muster",
|
||||
null, null, null, null, null, null, null);
|
||||
return Supplier.create(draft).unsafeGetValue();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue