1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 10:29:35 +01:00

refactor: OffsetDateTime-Migration, atomare Batch-Sequenznummern und Quantity.reconstitute-Overload

- LocalDateTime → OffsetDateTime (UTC) in allen Domain-Klassen, JPA Entities, DTOs und Tests
- Liquibase-Migration 017: TIMESTAMP → TIMESTAMP WITH TIME ZONE für bestehende Spalten
- Custom DateTimeProvider für Spring Data @CreatedDate-Kompatibilität mit OffsetDateTime
- Neue Sequenztabelle (016) mit JPA Entity + PESSIMISTIC_WRITE Lock statt COUNT-basierter
  Batch-Nummernvergabe (Race Condition Fix)
- Quantity.reconstitute(amount, uom) 2-Parameter-Overload für bessere Lesbarkeit
This commit is contained in:
Sebastian Frick 2026-02-20 00:40:58 +01:00
parent b06157b92c
commit b46495e1aa
64 changed files with 414 additions and 256 deletions

View file

@ -7,7 +7,8 @@ import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Optional; import java.util.Optional;
/** /**
@ -71,7 +72,7 @@ public class AuthenticateUser {
SessionToken token = sessionManager.createSession(user); SessionToken token = sessionManager.createSession(user);
// 5. Update last login timestamp (immutable) // 5. Update last login timestamp (immutable)
return user.withLastLogin(LocalDateTime.now()) return user.withLastLogin(OffsetDateTime.now(ZoneOffset.UTC))
.flatMap(updated -> userRepository.save(updated) .flatMap(updated -> userRepository.save(updated)
.mapError(err -> (UserError) new UserError.RepositoryFailure(err.message())) .mapError(err -> (UserError) new UserError.RepositoryFailure(err.message()))
.map(ignored -> { .map(ignored -> {

View file

@ -1,6 +1,7 @@
package de.effigenix.application.usermanagement.dto; package de.effigenix.application.usermanagement.dto;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
/** /**
* JWT session token returned after successful authentication. * JWT session token returned after successful authentication.
@ -9,7 +10,7 @@ public record SessionToken(
String accessToken, String accessToken,
String tokenType, String tokenType,
long expiresIn, // in seconds long expiresIn, // in seconds
LocalDateTime expiresAt, OffsetDateTime expiresAt,
String refreshToken // for future refresh token support String refreshToken // for future refresh token support
) { ) {
public static SessionToken create(String accessToken, long expiresInMs, String refreshToken) { public static SessionToken create(String accessToken, long expiresInMs, String refreshToken) {
@ -17,7 +18,7 @@ public record SessionToken(
accessToken, accessToken,
"Bearer", "Bearer",
expiresInMs / 1000, // convert to seconds expiresInMs / 1000, // convert to seconds
LocalDateTime.now().plusSeconds(expiresInMs / 1000), OffsetDateTime.now(ZoneOffset.UTC).plusSeconds(expiresInMs / 1000),
refreshToken refreshToken
); );
} }

View file

@ -3,7 +3,7 @@ package de.effigenix.application.usermanagement.dto;
import de.effigenix.domain.usermanagement.User; import de.effigenix.domain.usermanagement.User;
import de.effigenix.domain.usermanagement.UserStatus; import de.effigenix.domain.usermanagement.UserStatus;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -18,8 +18,8 @@ public record UserDTO(
Set<RoleDTO> roles, Set<RoleDTO> roles,
String branchId, String branchId,
UserStatus status, UserStatus status,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime lastLogin OffsetDateTime lastLogin
) { ) {
/** /**
* Maps a User entity to a UserDTO. * Maps a User entity to a UserDTO.

View file

@ -37,7 +37,7 @@ public record MinimumLevel(Quantity quantity) {
// MinimumLevel erlaubt amount == 0 (kein Mindestbestand) // MinimumLevel erlaubt amount == 0 (kein Mindestbestand)
// Quantity.of() verlangt amount > 0, daher Reconstitute verwenden // Quantity.of() verlangt amount > 0, daher Reconstitute verwenden
if (parsedAmount.compareTo(BigDecimal.ZERO) == 0) { if (parsedAmount.compareTo(BigDecimal.ZERO) == 0) {
return Result.success(new MinimumLevel(Quantity.reconstitute(parsedAmount, parsedUnit, null, null))); return Result.success(new MinimumLevel(Quantity.reconstitute(parsedAmount, parsedUnit)));
} }
return Quantity.of(parsedAmount, parsedUnit) return Quantity.of(parsedAmount, parsedUnit)

View file

@ -108,7 +108,7 @@ public class StockBatch {
return Result.failure(new StockError.NegativeStockNotAllowed()); return Result.failure(new StockError.NegativeStockNotAllowed());
} }
if (remaining.compareTo(BigDecimal.ZERO) == 0) { if (remaining.compareTo(BigDecimal.ZERO) == 0) {
return Result.success(Quantity.reconstitute(BigDecimal.ZERO, this.quantity.uom(), null, null)); return Result.success(Quantity.reconstitute(BigDecimal.ZERO, this.quantity.uom()));
} }
switch (Quantity.of(remaining, this.quantity.uom())) { switch (Quantity.of(remaining, this.quantity.uom())) {
case Result.Failure(var err) -> { case Result.Failure(var err) -> {

View file

@ -5,7 +5,8 @@ import de.effigenix.shared.common.Result;
import static de.effigenix.shared.common.Result.*; import static de.effigenix.shared.common.Result.*;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.*; import java.util.*;
/** /**
@ -26,8 +27,8 @@ public class Article {
private final List<SalesUnit> salesUnits; private final List<SalesUnit> salesUnits;
private ArticleStatus status; private ArticleStatus status;
private final Set<SupplierId> supplierReferences; private final Set<SupplierId> supplierReferences;
private final LocalDateTime createdAt; private final OffsetDateTime createdAt;
private LocalDateTime updatedAt; private OffsetDateTime updatedAt;
private Article( private Article(
ArticleId id, ArticleId id,
@ -37,8 +38,8 @@ public class Article {
List<SalesUnit> salesUnits, List<SalesUnit> salesUnits,
ArticleStatus status, ArticleStatus status,
Set<SupplierId> supplierReferences, Set<SupplierId> supplierReferences,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime updatedAt OffsetDateTime updatedAt
) { ) {
this.id = id; this.id = id;
this.name = name; this.name = name;
@ -88,7 +89,7 @@ public class Article {
case Success(var val) -> salesUnit = val; case Success(var val) -> salesUnit = val;
} }
var now = LocalDateTime.now(); var now = OffsetDateTime.now(ZoneOffset.UTC);
return Result.success(new Article( return Result.success(new Article(
ArticleId.generate(), ArticleId.generate(),
name, name,
@ -110,8 +111,8 @@ public class Article {
List<SalesUnit> salesUnits, List<SalesUnit> salesUnits,
ArticleStatus status, ArticleStatus status,
Set<SupplierId> supplierReferences, Set<SupplierId> supplierReferences,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime updatedAt OffsetDateTime updatedAt
) { ) {
return new Article(id, name, articleNumber, categoryId, salesUnits, status, return new Article(id, name, articleNumber, categoryId, salesUnits, status,
supplierReferences, createdAt, updatedAt); supplierReferences, createdAt, updatedAt);
@ -208,7 +209,7 @@ public class Article {
} }
private void touch() { private void touch() {
this.updatedAt = LocalDateTime.now(); this.updatedAt = OffsetDateTime.now(ZoneOffset.UTC);
} }
// ==================== Getters ==================== // ==================== Getters ====================
@ -220,8 +221,8 @@ public class Article {
public List<SalesUnit> salesUnits() { return Collections.unmodifiableList(salesUnits); } public List<SalesUnit> salesUnits() { return Collections.unmodifiableList(salesUnits); }
public ArticleStatus status() { return status; } public ArticleStatus status() { return status; }
public Set<SupplierId> supplierReferences() { return Collections.unmodifiableSet(supplierReferences); } public Set<SupplierId> supplierReferences() { return Collections.unmodifiableSet(supplierReferences); }
public LocalDateTime createdAt() { return createdAt; } public OffsetDateTime createdAt() { return createdAt; }
public LocalDateTime updatedAt() { return updatedAt; } public OffsetDateTime updatedAt() { return updatedAt; }
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {

View file

@ -8,7 +8,8 @@ import de.effigenix.shared.common.Result;
import static de.effigenix.shared.common.Result.*; import static de.effigenix.shared.common.Result.*;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.*; import java.util.*;
/** /**
@ -33,8 +34,8 @@ public class Customer {
private FrameContract frameContract; private FrameContract frameContract;
private final Set<CustomerPreference> preferences; private final Set<CustomerPreference> preferences;
private CustomerStatus status; private CustomerStatus status;
private final LocalDateTime createdAt; private final OffsetDateTime createdAt;
private LocalDateTime updatedAt; private OffsetDateTime updatedAt;
private Customer( private Customer(
CustomerId id, CustomerId id,
@ -47,8 +48,8 @@ public class Customer {
FrameContract frameContract, FrameContract frameContract,
Set<CustomerPreference> preferences, Set<CustomerPreference> preferences,
CustomerStatus status, CustomerStatus status,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime updatedAt OffsetDateTime updatedAt
) { ) {
this.id = id; this.id = id;
this.name = name; this.name = name;
@ -100,7 +101,7 @@ public class Customer {
} }
} }
var now = LocalDateTime.now(); var now = OffsetDateTime.now(ZoneOffset.UTC);
return Result.success(new Customer( return Result.success(new Customer(
CustomerId.generate(), name, draft.type(), billingAddress, contactInfo, paymentTerms, CustomerId.generate(), name, draft.type(), billingAddress, contactInfo, paymentTerms,
List.of(), null, Set.of(), CustomerStatus.ACTIVE, now, now List.of(), null, Set.of(), CustomerStatus.ACTIVE, now, now
@ -118,8 +119,8 @@ public class Customer {
FrameContract frameContract, FrameContract frameContract,
Set<CustomerPreference> preferences, Set<CustomerPreference> preferences,
CustomerStatus status, CustomerStatus status,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime updatedAt OffsetDateTime updatedAt
) { ) {
return new Customer(id, name, type, billingAddress, contactInfo, paymentTerms, return new Customer(id, name, type, billingAddress, contactInfo, paymentTerms,
deliveryAddresses, frameContract, preferences, status, createdAt, updatedAt); deliveryAddresses, frameContract, preferences, status, createdAt, updatedAt);
@ -235,7 +236,7 @@ public class Customer {
// ==================== Helpers ==================== // ==================== Helpers ====================
private void touch() { private void touch() {
this.updatedAt = LocalDateTime.now(); this.updatedAt = OffsetDateTime.now(ZoneOffset.UTC);
} }
// ==================== Getters ==================== // ==================== Getters ====================
@ -250,8 +251,8 @@ public class Customer {
public FrameContract frameContract() { return frameContract; } public FrameContract frameContract() { return frameContract; }
public Set<CustomerPreference> preferences() { return Collections.unmodifiableSet(preferences); } public Set<CustomerPreference> preferences() { return Collections.unmodifiableSet(preferences); }
public CustomerStatus status() { return status; } public CustomerStatus status() { return status; }
public LocalDateTime createdAt() { return createdAt; } public OffsetDateTime createdAt() { return createdAt; }
public LocalDateTime updatedAt() { return updatedAt; } public OffsetDateTime updatedAt() { return updatedAt; }
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {

View file

@ -5,7 +5,8 @@ import de.effigenix.shared.common.Address;
import de.effigenix.shared.common.ContactInfo; import de.effigenix.shared.common.ContactInfo;
import de.effigenix.shared.common.PaymentTerms; import de.effigenix.shared.common.PaymentTerms;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -31,8 +32,8 @@ public class Supplier {
private final List<QualityCertificate> certificates; private final List<QualityCertificate> certificates;
private SupplierRating rating; private SupplierRating rating;
private SupplierStatus status; private SupplierStatus status;
private final LocalDateTime createdAt; private final OffsetDateTime createdAt;
private LocalDateTime updatedAt; private OffsetDateTime updatedAt;
private Supplier( private Supplier(
SupplierId id, SupplierId id,
@ -43,8 +44,8 @@ public class Supplier {
List<QualityCertificate> certificates, List<QualityCertificate> certificates,
SupplierRating rating, SupplierRating rating,
SupplierStatus status, SupplierStatus status,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime updatedAt OffsetDateTime updatedAt
) { ) {
this.id = id; this.id = id;
this.name = name; this.name = name;
@ -96,7 +97,7 @@ public class Supplier {
} }
} }
var now = LocalDateTime.now(); var now = OffsetDateTime.now(ZoneOffset.UTC);
return Result.success(new Supplier( return Result.success(new Supplier(
SupplierId.generate(), name, address, contactInfo, paymentTerms, SupplierId.generate(), name, address, contactInfo, paymentTerms,
List.of(), null, SupplierStatus.ACTIVE, now, now List.of(), null, SupplierStatus.ACTIVE, now, now
@ -112,8 +113,8 @@ public class Supplier {
List<QualityCertificate> certificates, List<QualityCertificate> certificates,
SupplierRating rating, SupplierRating rating,
SupplierStatus status, SupplierStatus status,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime updatedAt OffsetDateTime updatedAt
) { ) {
return new Supplier(id, name, address, contactInfo, paymentTerms, return new Supplier(id, name, address, contactInfo, paymentTerms,
certificates, rating, status, createdAt, updatedAt); certificates, rating, status, createdAt, updatedAt);
@ -200,7 +201,7 @@ public class Supplier {
// ==================== Helpers ==================== // ==================== Helpers ====================
private void touch() { private void touch() {
this.updatedAt = LocalDateTime.now(); this.updatedAt = OffsetDateTime.now(ZoneOffset.UTC);
} }
// ==================== Getters ==================== // ==================== Getters ====================
@ -213,8 +214,8 @@ public class Supplier {
public List<QualityCertificate> certificates() { return Collections.unmodifiableList(certificates); } public List<QualityCertificate> certificates() { return Collections.unmodifiableList(certificates); }
public SupplierRating rating() { return rating; } public SupplierRating rating() { return rating; }
public SupplierStatus status() { return status; } public SupplierStatus status() { return status; }
public LocalDateTime createdAt() { return createdAt; } public OffsetDateTime createdAt() { return createdAt; }
public LocalDateTime updatedAt() { return updatedAt; } public OffsetDateTime updatedAt() { return updatedAt; }
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {

View file

@ -6,7 +6,8 @@ import de.effigenix.shared.common.UnitOfMeasure;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
/** /**
* Batch aggregate root. * Batch aggregate root.
@ -27,8 +28,8 @@ public class Batch {
private final Quantity plannedQuantity; private final Quantity plannedQuantity;
private final LocalDate productionDate; private final LocalDate productionDate;
private final LocalDate bestBeforeDate; private final LocalDate bestBeforeDate;
private final LocalDateTime createdAt; private final OffsetDateTime createdAt;
private LocalDateTime updatedAt; private OffsetDateTime updatedAt;
private Batch( private Batch(
BatchId id, BatchId id,
@ -38,8 +39,8 @@ public class Batch {
Quantity plannedQuantity, Quantity plannedQuantity,
LocalDate productionDate, LocalDate productionDate,
LocalDate bestBeforeDate, LocalDate bestBeforeDate,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime updatedAt OffsetDateTime updatedAt
) { ) {
this.id = id; this.id = id;
this.batchNumber = batchNumber; this.batchNumber = batchNumber;
@ -88,7 +89,7 @@ public class Batch {
"Invalid unit: " + draft.plannedQuantityUnit())); "Invalid unit: " + draft.plannedQuantityUnit()));
} }
var now = LocalDateTime.now(); var now = OffsetDateTime.now(ZoneOffset.UTC);
return Result.success(new Batch( return Result.success(new Batch(
BatchId.generate(), BatchId.generate(),
batchNumber, batchNumber,
@ -110,8 +111,8 @@ public class Batch {
Quantity plannedQuantity, Quantity plannedQuantity,
LocalDate productionDate, LocalDate productionDate,
LocalDate bestBeforeDate, LocalDate bestBeforeDate,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime updatedAt OffsetDateTime updatedAt
) { ) {
return new Batch(id, batchNumber, recipeId, status, plannedQuantity, productionDate, bestBeforeDate, createdAt, updatedAt); return new Batch(id, batchNumber, recipeId, status, plannedQuantity, productionDate, bestBeforeDate, createdAt, updatedAt);
} }
@ -123,6 +124,6 @@ public class Batch {
public Quantity plannedQuantity() { return plannedQuantity; } public Quantity plannedQuantity() { return plannedQuantity; }
public LocalDate productionDate() { return productionDate; } public LocalDate productionDate() { return productionDate; }
public LocalDate bestBeforeDate() { return bestBeforeDate; } public LocalDate bestBeforeDate() { return bestBeforeDate; }
public LocalDateTime createdAt() { return createdAt; } public OffsetDateTime createdAt() { return createdAt; }
public LocalDateTime updatedAt() { return updatedAt; } public OffsetDateTime updatedAt() { return updatedAt; }
} }

View file

@ -5,7 +5,8 @@ import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure; import de.effigenix.shared.common.UnitOfMeasure;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -44,8 +45,8 @@ public class Recipe {
private RecipeStatus status; private RecipeStatus status;
private final List<Ingredient> ingredients; private final List<Ingredient> ingredients;
private final List<ProductionStep> productionSteps; private final List<ProductionStep> productionSteps;
private final LocalDateTime createdAt; private final OffsetDateTime createdAt;
private LocalDateTime updatedAt; private OffsetDateTime updatedAt;
private Recipe( private Recipe(
RecipeId id, RecipeId id,
@ -59,8 +60,8 @@ public class Recipe {
RecipeStatus status, RecipeStatus status,
List<Ingredient> ingredients, List<Ingredient> ingredients,
List<ProductionStep> productionSteps, List<ProductionStep> productionSteps,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime updatedAt OffsetDateTime updatedAt
) { ) {
this.id = id; this.id = id;
this.name = name; this.name = name;
@ -123,7 +124,7 @@ public class Recipe {
return Result.failure(new RecipeError.ValidationFailure("Invalid output quantity: " + e.getMessage())); return Result.failure(new RecipeError.ValidationFailure("Invalid output quantity: " + e.getMessage()));
} }
var now = LocalDateTime.now(); var now = OffsetDateTime.now(ZoneOffset.UTC);
return Result.success(new Recipe( return Result.success(new Recipe(
RecipeId.generate(), name, draft.version(), draft.type(), RecipeId.generate(), name, draft.version(), draft.type(),
draft.description(), yieldPercentage, shelfLifeDays, outputQuantity, draft.description(), yieldPercentage, shelfLifeDays, outputQuantity,
@ -146,8 +147,8 @@ public class Recipe {
RecipeStatus status, RecipeStatus status,
List<Ingredient> ingredients, List<Ingredient> ingredients,
List<ProductionStep> productionSteps, List<ProductionStep> productionSteps,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime updatedAt OffsetDateTime updatedAt
) { ) {
return new Recipe(id, name, version, type, description, return new Recipe(id, name, version, type, description,
yieldPercentage, shelfLifeDays, outputQuantity, status, ingredients, productionSteps, createdAt, updatedAt); yieldPercentage, shelfLifeDays, outputQuantity, status, ingredients, productionSteps, createdAt, updatedAt);
@ -255,8 +256,8 @@ public class Recipe {
public RecipeStatus status() { return status; } public RecipeStatus status() { return status; }
public List<Ingredient> ingredients() { return Collections.unmodifiableList(ingredients); } public List<Ingredient> ingredients() { return Collections.unmodifiableList(ingredients); }
public List<ProductionStep> productionSteps() { return Collections.unmodifiableList(productionSteps); } public List<ProductionStep> productionSteps() { return Collections.unmodifiableList(productionSteps); }
public LocalDateTime createdAt() { return createdAt; } public OffsetDateTime createdAt() { return createdAt; }
public LocalDateTime updatedAt() { return updatedAt; } public OffsetDateTime updatedAt() { return updatedAt; }
// ==================== Helpers ==================== // ==================== Helpers ====================
@ -269,7 +270,7 @@ public class Recipe {
} }
private void touch() { private void touch() {
this.updatedAt = LocalDateTime.now(); this.updatedAt = OffsetDateTime.now(ZoneOffset.UTC);
} }
@Override @Override

View file

@ -2,7 +2,8 @@ package de.effigenix.domain.usermanagement;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@ -33,8 +34,8 @@ public class User {
private final Set<Role> roles; private final Set<Role> roles;
private final String branchId; private final String branchId;
private final UserStatus status; private final UserStatus status;
private final LocalDateTime createdAt; private final OffsetDateTime createdAt;
private final LocalDateTime lastLogin; private final OffsetDateTime lastLogin;
private User( private User(
UserId id, UserId id,
@ -44,8 +45,8 @@ public class User {
Set<Role> roles, Set<Role> roles,
String branchId, String branchId,
UserStatus status, UserStatus status,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime lastLogin OffsetDateTime lastLogin
) { ) {
this.id = id; this.id = id;
this.username = username; this.username = username;
@ -54,7 +55,7 @@ public class User {
this.roles = roles != null ? Set.copyOf(roles) : Set.of(); this.roles = roles != null ? Set.copyOf(roles) : Set.of();
this.branchId = branchId; this.branchId = branchId;
this.status = status; this.status = status;
this.createdAt = createdAt != null ? createdAt : LocalDateTime.now(); this.createdAt = createdAt != null ? createdAt : OffsetDateTime.now(ZoneOffset.UTC);
this.lastLogin = lastLogin; this.lastLogin = lastLogin;
} }
@ -86,7 +87,7 @@ public class User {
roles, roles,
branchId, branchId,
UserStatus.ACTIVE, UserStatus.ACTIVE,
LocalDateTime.now(), OffsetDateTime.now(ZoneOffset.UTC),
null null
)); ));
} }
@ -102,15 +103,15 @@ public class User {
Set<Role> roles, Set<Role> roles,
String branchId, String branchId,
UserStatus status, UserStatus status,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime lastLogin OffsetDateTime lastLogin
) { ) {
return new User(id, username, email, passwordHash, roles, branchId, status, createdAt, lastLogin); return new User(id, username, email, passwordHash, roles, branchId, status, createdAt, lastLogin);
} }
// ==================== Business Methods (Wither-Pattern) ==================== // ==================== Business Methods (Wither-Pattern) ====================
public Result<UserError, User> withLastLogin(LocalDateTime timestamp) { public Result<UserError, User> withLastLogin(OffsetDateTime timestamp) {
return Result.success(new User(id, username, email, passwordHash, roles, branchId, status, createdAt, timestamp)); return Result.success(new User(id, username, email, passwordHash, roles, branchId, status, createdAt, timestamp));
} }
@ -218,8 +219,8 @@ public class User {
public Set<Role> roles() { return Collections.unmodifiableSet(roles); } public Set<Role> roles() { return Collections.unmodifiableSet(roles); }
public String branchId() { return branchId; } public String branchId() { return branchId; }
public UserStatus status() { return status; } public UserStatus status() { return status; }
public LocalDateTime createdAt() { return createdAt; } public OffsetDateTime createdAt() { return createdAt; }
public LocalDateTime lastLogin() { return lastLogin; } public OffsetDateTime lastLogin() { return lastLogin; }
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {

View file

@ -5,7 +5,8 @@ import jakarta.persistence.*;
import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
/** /**
* JPA Entity for Audit Logs. * JPA Entity for Audit Logs.
@ -43,7 +44,7 @@ public class AuditLogEntity {
private String details; private String details;
@Column(name = "timestamp", nullable = false) @Column(name = "timestamp", nullable = false)
private LocalDateTime timestamp; private OffsetDateTime timestamp;
@Column(name = "ip_address", length = 45) // IPv6 max length @Column(name = "ip_address", length = 45) // IPv6 max length
private String ipAddress; private String ipAddress;
@ -53,7 +54,7 @@ public class AuditLogEntity {
@CreatedDate @CreatedDate
@Column(name = "created_at", nullable = false, updatable = false) @Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt; private OffsetDateTime createdAt;
// JPA requires no-arg constructor // JPA requires no-arg constructor
protected AuditLogEntity() { protected AuditLogEntity() {
@ -65,7 +66,7 @@ public class AuditLogEntity {
String entityId, String entityId,
String performedBy, String performedBy,
String details, String details,
LocalDateTime timestamp, OffsetDateTime timestamp,
String ipAddress, String ipAddress,
String userAgent String userAgent
) { ) {
@ -77,7 +78,7 @@ public class AuditLogEntity {
this.timestamp = timestamp; this.timestamp = timestamp;
this.ipAddress = ipAddress; this.ipAddress = ipAddress;
this.userAgent = userAgent; this.userAgent = userAgent;
this.createdAt = LocalDateTime.now(); this.createdAt = OffsetDateTime.now(ZoneOffset.UTC);
} }
// Getters only (immutable after creation) // Getters only (immutable after creation)
@ -101,7 +102,7 @@ public class AuditLogEntity {
return details; return details;
} }
public LocalDateTime getTimestamp() { public OffsetDateTime getTimestamp() {
return timestamp; return timestamp;
} }
@ -113,7 +114,7 @@ public class AuditLogEntity {
return userAgent; return userAgent;
} }
public LocalDateTime getCreatedAt() { public OffsetDateTime getCreatedAt() {
return createdAt; return createdAt;
} }
} }

View file

@ -4,7 +4,7 @@ import de.effigenix.application.usermanagement.AuditEvent;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.List; import java.util.List;
/** /**
@ -34,7 +34,7 @@ public interface AuditLogJpaRepository extends JpaRepository<AuditLogEntity, Str
/** /**
* Finds all audit logs within a time range. * Finds all audit logs within a time range.
*/ */
List<AuditLogEntity> findByTimestampBetween(LocalDateTime start, LocalDateTime end); List<AuditLogEntity> findByTimestampBetween(OffsetDateTime start, OffsetDateTime end);
/** /**
* Finds all audit logs for a specific event and actor. * Finds all audit logs for a specific event and actor.

View file

@ -13,7 +13,8 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.UUID; import java.util.UUID;
/** /**
@ -52,7 +53,7 @@ public class DatabaseAuditLogger implements AuditLogger {
entityId, entityId,
performedBy.value(), performedBy.value(),
null, // no additional details null, // no additional details
LocalDateTime.now(), OffsetDateTime.now(ZoneOffset.UTC),
getClientIpAddress(), getClientIpAddress(),
getUserAgent() getUserAgent()
); );
@ -75,7 +76,7 @@ public class DatabaseAuditLogger implements AuditLogger {
null, // no entity ID null, // no entity ID
null, // no actor (e.g., system event) null, // no actor (e.g., system event)
details, details,
LocalDateTime.now(), OffsetDateTime.now(ZoneOffset.UTC),
getClientIpAddress(), getClientIpAddress(),
getUserAgent() getUserAgent()
); );
@ -97,7 +98,7 @@ public class DatabaseAuditLogger implements AuditLogger {
entityId, entityId,
performedBy.value(), performedBy.value(),
details, details,
LocalDateTime.now(), OffsetDateTime.now(ZoneOffset.UTC),
getClientIpAddress(), getClientIpAddress(),
getUserAgent() getUserAgent()
); );
@ -119,7 +120,7 @@ public class DatabaseAuditLogger implements AuditLogger {
null, // no entity ID null, // no entity ID
performedBy.value(), performedBy.value(),
null, // no additional details null, // no additional details
LocalDateTime.now(), OffsetDateTime.now(ZoneOffset.UTC),
getClientIpAddress(), getClientIpAddress(),
getUserAgent() getUserAgent()
); );

View file

@ -1,14 +1,26 @@
package de.effigenix.infrastructure.config; package de.effigenix.infrastructure.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.data.auditing.DateTimeProvider;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Optional;
/** /**
* Aktiviert JPA-Auditing nur, wenn eine Datenbankverbindung vorhanden ist. * Aktiviert JPA-Auditing nur, wenn eine Datenbankverbindung vorhanden ist.
* Verwendet OffsetDateTime (UTC) statt LocalDateTime für @CreatedDate/@LastModifiedDate.
*/ */
@Configuration @Configuration
@Profile("!no-db") @Profile("!no-db")
@EnableJpaAuditing @EnableJpaAuditing(dateTimeProviderRef = "utcDateTimeProvider")
public class JpaAuditingConfig { public class JpaAuditingConfig {
@Bean
public DateTimeProvider utcDateTimeProvider() {
return () -> Optional.of(OffsetDateTime.now(ZoneOffset.UTC));
}
} }

View file

@ -42,8 +42,7 @@ public class StockMapper {
if (entity.getMinimumLevelAmount() != null && entity.getMinimumLevelUnit() != null) { if (entity.getMinimumLevelAmount() != null && entity.getMinimumLevelUnit() != null) {
var quantity = Quantity.reconstitute( var quantity = Quantity.reconstitute(
entity.getMinimumLevelAmount(), entity.getMinimumLevelAmount(),
UnitOfMeasure.valueOf(entity.getMinimumLevelUnit()), UnitOfMeasure.valueOf(entity.getMinimumLevelUnit())
null, null
); );
minimumLevel = new MinimumLevel(quantity); minimumLevel = new MinimumLevel(quantity);
} }
@ -87,8 +86,7 @@ public class StockMapper {
new BatchReference(entity.getBatchId(), BatchType.valueOf(entity.getBatchType())), new BatchReference(entity.getBatchId(), BatchType.valueOf(entity.getBatchType())),
Quantity.reconstitute( Quantity.reconstitute(
entity.getQuantityAmount(), entity.getQuantityAmount(),
UnitOfMeasure.valueOf(entity.getQuantityUnit()), UnitOfMeasure.valueOf(entity.getQuantityUnit())
null, null
), ),
entity.getExpiryDate(), entity.getExpiryDate(),
StockBatchStatus.valueOf(entity.getStatus()), StockBatchStatus.valueOf(entity.getStatus()),

View file

@ -2,7 +2,7 @@ package de.effigenix.infrastructure.masterdata.persistence.entity;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -29,10 +29,10 @@ public class ArticleEntity {
private String status; private String status;
@Column(name = "created_at", nullable = false, updatable = false) @Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt; private OffsetDateTime createdAt;
@Column(name = "updated_at", nullable = false) @Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt; private OffsetDateTime updatedAt;
@OneToMany(mappedBy = "article", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) @OneToMany(mappedBy = "article", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
private List<SalesUnitEntity> salesUnits = new ArrayList<>(); private List<SalesUnitEntity> salesUnits = new ArrayList<>();
@ -45,7 +45,7 @@ public class ArticleEntity {
protected ArticleEntity() {} protected ArticleEntity() {}
public ArticleEntity(String id, String name, String articleNumber, String categoryId, public ArticleEntity(String id, String name, String articleNumber, String categoryId,
String status, LocalDateTime createdAt, LocalDateTime updatedAt) { String status, OffsetDateTime createdAt, OffsetDateTime updatedAt) {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.articleNumber = articleNumber; this.articleNumber = articleNumber;
@ -60,8 +60,8 @@ public class ArticleEntity {
public String getArticleNumber() { return articleNumber; } public String getArticleNumber() { return articleNumber; }
public String getCategoryId() { return categoryId; } public String getCategoryId() { return categoryId; }
public String getStatus() { return status; } public String getStatus() { return status; }
public LocalDateTime getCreatedAt() { return createdAt; } public OffsetDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; } public OffsetDateTime getUpdatedAt() { return updatedAt; }
public List<SalesUnitEntity> getSalesUnits() { return salesUnits; } public List<SalesUnitEntity> getSalesUnits() { return salesUnits; }
public Set<String> getSupplierIds() { return supplierIds; } public Set<String> getSupplierIds() { return supplierIds; }
@ -70,8 +70,8 @@ public class ArticleEntity {
public void setArticleNumber(String articleNumber) { this.articleNumber = articleNumber; } public void setArticleNumber(String articleNumber) { this.articleNumber = articleNumber; }
public void setCategoryId(String categoryId) { this.categoryId = categoryId; } public void setCategoryId(String categoryId) { this.categoryId = categoryId; }
public void setStatus(String status) { this.status = status; } public void setStatus(String status) { this.status = status; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } public void setUpdatedAt(OffsetDateTime updatedAt) { this.updatedAt = updatedAt; }
public void setSalesUnits(List<SalesUnitEntity> salesUnits) { this.salesUnits = salesUnits; } public void setSalesUnits(List<SalesUnitEntity> salesUnits) { this.salesUnits = salesUnits; }
public void setSupplierIds(Set<String> supplierIds) { this.supplierIds = supplierIds; } public void setSupplierIds(Set<String> supplierIds) { this.supplierIds = supplierIds; }
} }

View file

@ -2,7 +2,7 @@ package de.effigenix.infrastructure.masterdata.persistence.entity;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -56,10 +56,10 @@ public class CustomerEntity {
private String status; private String status;
@Column(name = "created_at", nullable = false, updatable = false) @Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt; private OffsetDateTime createdAt;
@Column(name = "updated_at", nullable = false) @Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt; private OffsetDateTime updatedAt;
@ElementCollection(fetch = FetchType.EAGER) @ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "delivery_addresses", joinColumns = @JoinColumn(name = "customer_id")) @CollectionTable(name = "delivery_addresses", joinColumns = @JoinColumn(name = "customer_id"))
@ -91,8 +91,8 @@ public class CustomerEntity {
public Integer getPaymentDueDays() { return paymentDueDays; } public Integer getPaymentDueDays() { return paymentDueDays; }
public String getPaymentDescription() { return paymentDescription; } public String getPaymentDescription() { return paymentDescription; }
public String getStatus() { return status; } public String getStatus() { return status; }
public LocalDateTime getCreatedAt() { return createdAt; } public OffsetDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; } public OffsetDateTime getUpdatedAt() { return updatedAt; }
public List<DeliveryAddressEmbeddable> getDeliveryAddresses() { return deliveryAddresses; } public List<DeliveryAddressEmbeddable> getDeliveryAddresses() { return deliveryAddresses; }
public Set<de.effigenix.domain.masterdata.CustomerPreference> getPreferences() { return preferences; } public Set<de.effigenix.domain.masterdata.CustomerPreference> getPreferences() { return preferences; }
public FrameContractEntity getFrameContract() { return frameContract; } public FrameContractEntity getFrameContract() { return frameContract; }
@ -111,8 +111,8 @@ public class CustomerEntity {
public void setPaymentDueDays(Integer paymentDueDays) { this.paymentDueDays = paymentDueDays; } public void setPaymentDueDays(Integer paymentDueDays) { this.paymentDueDays = paymentDueDays; }
public void setPaymentDescription(String paymentDescription) { this.paymentDescription = paymentDescription; } public void setPaymentDescription(String paymentDescription) { this.paymentDescription = paymentDescription; }
public void setStatus(String status) { this.status = status; } public void setStatus(String status) { this.status = status; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } public void setUpdatedAt(OffsetDateTime updatedAt) { this.updatedAt = updatedAt; }
public void setDeliveryAddresses(List<DeliveryAddressEmbeddable> deliveryAddresses) { this.deliveryAddresses = deliveryAddresses; } public void setDeliveryAddresses(List<DeliveryAddressEmbeddable> deliveryAddresses) { this.deliveryAddresses = deliveryAddresses; }
public void setPreferences(Set<de.effigenix.domain.masterdata.CustomerPreference> preferences) { this.preferences = preferences; } public void setPreferences(Set<de.effigenix.domain.masterdata.CustomerPreference> preferences) { this.preferences = preferences; }
public void setFrameContract(FrameContractEntity frameContract) { this.frameContract = frameContract; } public void setFrameContract(FrameContractEntity frameContract) { this.frameContract = frameContract; }

View file

@ -2,7 +2,7 @@ package de.effigenix.infrastructure.masterdata.persistence.entity;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -60,10 +60,10 @@ public class SupplierEntity {
private String status; private String status;
@Column(name = "created_at", nullable = false, updatable = false) @Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt; private OffsetDateTime createdAt;
@Column(name = "updated_at", nullable = false) @Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt; private OffsetDateTime updatedAt;
@ElementCollection(fetch = FetchType.EAGER) @ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "quality_certificates", joinColumns = @JoinColumn(name = "supplier_id")) @CollectionTable(name = "quality_certificates", joinColumns = @JoinColumn(name = "supplier_id"))
@ -87,8 +87,8 @@ public class SupplierEntity {
public Integer getDeliveryScore() { return deliveryScore; } public Integer getDeliveryScore() { return deliveryScore; }
public Integer getPriceScore() { return priceScore; } public Integer getPriceScore() { return priceScore; }
public String getStatus() { return status; } public String getStatus() { return status; }
public LocalDateTime getCreatedAt() { return createdAt; } public OffsetDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; } public OffsetDateTime getUpdatedAt() { return updatedAt; }
public List<QualityCertificateEmbeddable> getCertificates() { return certificates; } public List<QualityCertificateEmbeddable> getCertificates() { return certificates; }
public void setId(String id) { this.id = id; } public void setId(String id) { this.id = id; }
@ -107,7 +107,7 @@ public class SupplierEntity {
public void setDeliveryScore(Integer deliveryScore) { this.deliveryScore = deliveryScore; } public void setDeliveryScore(Integer deliveryScore) { this.deliveryScore = deliveryScore; }
public void setPriceScore(Integer priceScore) { this.priceScore = priceScore; } public void setPriceScore(Integer priceScore) { this.priceScore = priceScore; }
public void setStatus(String status) { this.status = status; } public void setStatus(String status) { this.status = status; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } public void setUpdatedAt(OffsetDateTime updatedAt) { this.updatedAt = updatedAt; }
public void setCertificates(List<QualityCertificateEmbeddable> certificates) { this.certificates = certificates; } public void setCertificates(List<QualityCertificateEmbeddable> certificates) { this.certificates = certificates; }
} }

View file

@ -4,7 +4,7 @@ import de.effigenix.domain.masterdata.Article;
import de.effigenix.domain.masterdata.SupplierId; import de.effigenix.domain.masterdata.SupplierId;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.List; import java.util.List;
@Schema(requiredProperties = {"id", "name", "articleNumber", "categoryId", "salesUnits", "status", "supplierIds", "createdAt", "updatedAt"}) @Schema(requiredProperties = {"id", "name", "articleNumber", "categoryId", "salesUnits", "status", "supplierIds", "createdAt", "updatedAt"})
@ -16,8 +16,8 @@ public record ArticleResponse(
List<SalesUnitResponse> salesUnits, List<SalesUnitResponse> salesUnits,
String status, String status,
List<String> supplierIds, List<String> supplierIds,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime updatedAt OffsetDateTime updatedAt
) { ) {
public static ArticleResponse from(Article article) { public static ArticleResponse from(Article article) {
return new ArticleResponse( return new ArticleResponse(

View file

@ -4,7 +4,7 @@ import de.effigenix.domain.masterdata.Customer;
import de.effigenix.domain.masterdata.CustomerPreference; import de.effigenix.domain.masterdata.CustomerPreference;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.List; import java.util.List;
@Schema(requiredProperties = {"id", "name", "type", "billingAddress", "contactInfo", "deliveryAddresses", "preferences", "status", "createdAt", "updatedAt"}) @Schema(requiredProperties = {"id", "name", "type", "billingAddress", "contactInfo", "deliveryAddresses", "preferences", "status", "createdAt", "updatedAt"})
@ -19,8 +19,8 @@ public record CustomerResponse(
@Schema(nullable = true) FrameContractResponse frameContract, @Schema(nullable = true) FrameContractResponse frameContract,
List<String> preferences, List<String> preferences,
String status, String status,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime updatedAt OffsetDateTime updatedAt
) { ) {
public static CustomerResponse from(Customer customer) { public static CustomerResponse from(Customer customer) {
return new CustomerResponse( return new CustomerResponse(

View file

@ -3,7 +3,7 @@ package de.effigenix.infrastructure.masterdata.web.dto;
import de.effigenix.domain.masterdata.Supplier; import de.effigenix.domain.masterdata.Supplier;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.List; import java.util.List;
@Schema(requiredProperties = {"id", "name", "contactInfo", "certificates", "status", "createdAt", "updatedAt"}) @Schema(requiredProperties = {"id", "name", "contactInfo", "certificates", "status", "createdAt", "updatedAt"})
@ -16,8 +16,8 @@ public record SupplierResponse(
List<QualityCertificateResponse> certificates, List<QualityCertificateResponse> certificates,
@Schema(nullable = true) SupplierRatingResponse rating, @Schema(nullable = true) SupplierRatingResponse rating,
String status, String status,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime updatedAt OffsetDateTime updatedAt
) { ) {
public static SupplierResponse from(Supplier supplier) { public static SupplierResponse from(Supplier supplier) {
return new SupplierResponse( return new SupplierResponse(

View file

@ -3,10 +3,12 @@ package de.effigenix.infrastructure.production.persistence;
import de.effigenix.domain.production.BatchError; import de.effigenix.domain.production.BatchError;
import de.effigenix.domain.production.BatchNumber; import de.effigenix.domain.production.BatchNumber;
import de.effigenix.domain.production.BatchNumberGenerator; import de.effigenix.domain.production.BatchNumberGenerator;
import de.effigenix.infrastructure.production.persistence.repository.BatchJpaRepository; import de.effigenix.infrastructure.production.persistence.entity.BatchNumberSequenceEntity;
import de.effigenix.infrastructure.production.persistence.repository.BatchNumberSequenceJpaRepository;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate; import java.time.LocalDate;
@ -14,17 +16,25 @@ import java.time.LocalDate;
@Profile("!no-db") @Profile("!no-db")
public class JpaBatchNumberGenerator implements BatchNumberGenerator { public class JpaBatchNumberGenerator implements BatchNumberGenerator {
private final BatchJpaRepository batchJpaRepository; private final BatchNumberSequenceJpaRepository sequenceRepository;
public JpaBatchNumberGenerator(BatchJpaRepository batchJpaRepository) { public JpaBatchNumberGenerator(BatchNumberSequenceJpaRepository sequenceRepository) {
this.batchJpaRepository = batchJpaRepository; this.sequenceRepository = sequenceRepository;
} }
@Override @Override
@Transactional
public Result<BatchError, BatchNumber> generateNext(LocalDate date) { public Result<BatchError, BatchNumber> generateNext(LocalDate date) {
try { try {
int count = batchJpaRepository.countByProductionDate(date); var sequence = sequenceRepository.findByProductionDate(date);
int nextSequence = count + 1; int nextSequence;
if (sequence.isPresent()) {
nextSequence = sequence.get().getLastSequence() + 1;
sequence.get().setLastSequence(nextSequence);
} else {
nextSequence = 1;
sequenceRepository.save(new BatchNumberSequenceEntity(date, 1));
}
if (nextSequence > 999) { if (nextSequence > 999) {
return Result.failure(new BatchError.ValidationFailure( return Result.failure(new BatchError.ValidationFailure(
"Maximum batch number sequence (999) reached for date " + date)); "Maximum batch number sequence (999) reached for date " + date));

View file

@ -4,7 +4,7 @@ import jakarta.persistence.*;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
@Entity @Entity
@Table(name = "batches") @Table(name = "batches")
@ -36,10 +36,10 @@ public class BatchEntity {
private LocalDate bestBeforeDate; private LocalDate bestBeforeDate;
@Column(name = "created_at", nullable = false, updatable = false) @Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt; private OffsetDateTime createdAt;
@Column(name = "updated_at", nullable = false) @Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt; private OffsetDateTime updatedAt;
protected BatchEntity() {} protected BatchEntity() {}
@ -52,8 +52,8 @@ public class BatchEntity {
String plannedQuantityUnit, String plannedQuantityUnit,
LocalDate productionDate, LocalDate productionDate,
LocalDate bestBeforeDate, LocalDate bestBeforeDate,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime updatedAt OffsetDateTime updatedAt
) { ) {
this.id = id; this.id = id;
this.batchNumber = batchNumber; this.batchNumber = batchNumber;
@ -75,6 +75,6 @@ public class BatchEntity {
public String getPlannedQuantityUnit() { return plannedQuantityUnit; } public String getPlannedQuantityUnit() { return plannedQuantityUnit; }
public LocalDate getProductionDate() { return productionDate; } public LocalDate getProductionDate() { return productionDate; }
public LocalDate getBestBeforeDate() { return bestBeforeDate; } public LocalDate getBestBeforeDate() { return bestBeforeDate; }
public LocalDateTime getCreatedAt() { return createdAt; } public OffsetDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; } public OffsetDateTime getUpdatedAt() { return updatedAt; }
} }

View file

@ -0,0 +1,37 @@
package de.effigenix.infrastructure.production.persistence.entity;
import jakarta.persistence.*;
import java.time.LocalDate;
@Entity
@Table(name = "batch_number_sequences")
public class BatchNumberSequenceEntity {
@Id
@Column(name = "production_date", nullable = false)
private LocalDate productionDate;
@Column(name = "last_sequence", nullable = false)
private int lastSequence;
protected BatchNumberSequenceEntity() {
}
public BatchNumberSequenceEntity(LocalDate productionDate, int lastSequence) {
this.productionDate = productionDate;
this.lastSequence = lastSequence;
}
public LocalDate getProductionDate() {
return productionDate;
}
public int getLastSequence() {
return lastSequence;
}
public void setLastSequence(int lastSequence) {
this.lastSequence = lastSequence;
}
}

View file

@ -3,7 +3,7 @@ package de.effigenix.infrastructure.production.persistence.entity;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -44,10 +44,10 @@ public class RecipeEntity {
private String status; private String status;
@Column(name = "created_at", nullable = false, updatable = false) @Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt; private OffsetDateTime createdAt;
@Column(name = "updated_at", nullable = false) @Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt; private OffsetDateTime updatedAt;
@OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
@OrderBy("position ASC") @OrderBy("position ASC")
@ -61,7 +61,7 @@ public class RecipeEntity {
public RecipeEntity(String id, String name, int version, String type, String description, public RecipeEntity(String id, String name, int version, String type, String description,
int yieldPercentage, Integer shelfLifeDays, BigDecimal outputQuantity, int yieldPercentage, Integer shelfLifeDays, BigDecimal outputQuantity,
String outputUom, String status, LocalDateTime createdAt, LocalDateTime updatedAt) { String outputUom, String status, OffsetDateTime createdAt, OffsetDateTime updatedAt) {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.version = version; this.version = version;
@ -86,8 +86,8 @@ public class RecipeEntity {
public BigDecimal getOutputQuantity() { return outputQuantity; } public BigDecimal getOutputQuantity() { return outputQuantity; }
public String getOutputUom() { return outputUom; } public String getOutputUom() { return outputUom; }
public String getStatus() { return status; } public String getStatus() { return status; }
public LocalDateTime getCreatedAt() { return createdAt; } public OffsetDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; } public OffsetDateTime getUpdatedAt() { return updatedAt; }
public List<IngredientEntity> getIngredients() { return ingredients; } public List<IngredientEntity> getIngredients() { return ingredients; }
public List<ProductionStepEntity> getProductionSteps() { return productionSteps; } public List<ProductionStepEntity> getProductionSteps() { return productionSteps; }
@ -101,8 +101,8 @@ public class RecipeEntity {
public void setOutputQuantity(BigDecimal outputQuantity) { this.outputQuantity = outputQuantity; } public void setOutputQuantity(BigDecimal outputQuantity) { this.outputQuantity = outputQuantity; }
public void setOutputUom(String outputUom) { this.outputUom = outputUom; } public void setOutputUom(String outputUom) { this.outputUom = outputUom; }
public void setStatus(String status) { this.status = status; } public void setStatus(String status) { this.status = status; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } public void setUpdatedAt(OffsetDateTime updatedAt) { this.updatedAt = updatedAt; }
public void setIngredients(List<IngredientEntity> ingredients) { this.ingredients = ingredients; } public void setIngredients(List<IngredientEntity> ingredients) { this.ingredients = ingredients; }
public void setProductionSteps(List<ProductionStepEntity> productionSteps) { this.productionSteps = productionSteps; } public void setProductionSteps(List<ProductionStepEntity> productionSteps) { this.productionSteps = productionSteps; }
} }

View file

@ -32,8 +32,7 @@ public class BatchMapper {
BatchStatus.valueOf(entity.getStatus()), BatchStatus.valueOf(entity.getStatus()),
Quantity.reconstitute( Quantity.reconstitute(
entity.getPlannedQuantityAmount(), entity.getPlannedQuantityAmount(),
UnitOfMeasure.valueOf(entity.getPlannedQuantityUnit()), UnitOfMeasure.valueOf(entity.getPlannedQuantityUnit())
null, null
), ),
entity.getProductionDate(), entity.getProductionDate(),
entity.getBestBeforeDate(), entity.getBestBeforeDate(),

View file

@ -62,8 +62,7 @@ public class RecipeMapper {
entity.getShelfLifeDays(), entity.getShelfLifeDays(),
Quantity.reconstitute( Quantity.reconstitute(
entity.getOutputQuantity(), entity.getOutputQuantity(),
UnitOfMeasure.valueOf(entity.getOutputUom()), UnitOfMeasure.valueOf(entity.getOutputUom())
null, null
), ),
RecipeStatus.valueOf(entity.getStatus()), RecipeStatus.valueOf(entity.getStatus()),
ingredients, ingredients,
@ -114,8 +113,7 @@ public class RecipeMapper {
entity.getArticleId(), entity.getArticleId(),
Quantity.reconstitute( Quantity.reconstitute(
entity.getQuantity(), entity.getQuantity(),
UnitOfMeasure.valueOf(entity.getUom()), UnitOfMeasure.valueOf(entity.getUom())
null, null
), ),
entity.getSubRecipeId(), entity.getSubRecipeId(),
entity.isSubstitutable() entity.isSubstitutable()

View file

@ -2,12 +2,6 @@ package de.effigenix.infrastructure.production.persistence.repository;
import de.effigenix.infrastructure.production.persistence.entity.BatchEntity; import de.effigenix.infrastructure.production.persistence.entity.BatchEntity;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.time.LocalDate;
public interface BatchJpaRepository extends JpaRepository<BatchEntity, String> { public interface BatchJpaRepository extends JpaRepository<BatchEntity, String> {
@Query("SELECT COUNT(b) FROM BatchEntity b WHERE b.productionDate = :date")
int countByProductionDate(LocalDate date);
} }

View file

@ -0,0 +1,15 @@
package de.effigenix.infrastructure.production.persistence.repository;
import de.effigenix.infrastructure.production.persistence.entity.BatchNumberSequenceEntity;
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import java.time.LocalDate;
import java.util.Optional;
public interface BatchNumberSequenceJpaRepository extends JpaRepository<BatchNumberSequenceEntity, LocalDate> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<BatchNumberSequenceEntity> findByProductionDate(LocalDate productionDate);
}

View file

@ -3,7 +3,7 @@ package de.effigenix.infrastructure.production.web.dto;
import de.effigenix.domain.production.Batch; import de.effigenix.domain.production.Batch;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
public record BatchResponse( public record BatchResponse(
String id, String id,
@ -14,8 +14,8 @@ public record BatchResponse(
String plannedQuantityUnit, String plannedQuantityUnit,
LocalDate productionDate, LocalDate productionDate,
LocalDate bestBeforeDate, LocalDate bestBeforeDate,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime updatedAt OffsetDateTime updatedAt
) { ) {
public static BatchResponse from(Batch batch) { public static BatchResponse from(Batch batch) {
return new BatchResponse( return new BatchResponse(

View file

@ -3,7 +3,7 @@ package de.effigenix.infrastructure.production.web.dto;
import de.effigenix.domain.production.Recipe; import de.effigenix.domain.production.Recipe;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.List; import java.util.List;
@Schema(requiredProperties = {"id", "name", "version", "type", "description", "yieldPercentage", "outputQuantity", "outputUom", "status", "ingredients", "productionSteps", "createdAt", "updatedAt"}) @Schema(requiredProperties = {"id", "name", "version", "type", "description", "yieldPercentage", "outputQuantity", "outputUom", "status", "ingredients", "productionSteps", "createdAt", "updatedAt"})
@ -20,8 +20,8 @@ public record RecipeResponse(
String status, String status,
List<IngredientResponse> ingredients, List<IngredientResponse> ingredients,
List<ProductionStepResponse> productionSteps, List<ProductionStepResponse> productionSteps,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime updatedAt OffsetDateTime updatedAt
) { ) {
public static RecipeResponse from(Recipe recipe) { public static RecipeResponse from(Recipe recipe) {
return new RecipeResponse( return new RecipeResponse(

View file

@ -3,7 +3,7 @@ package de.effigenix.infrastructure.production.web.dto;
import de.effigenix.domain.production.Recipe; import de.effigenix.domain.production.Recipe;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
@Schema(requiredProperties = {"id", "name", "version", "type", "description", "yieldPercentage", "outputQuantity", "outputUom", "status", "ingredientCount", "stepCount", "createdAt", "updatedAt"}) @Schema(requiredProperties = {"id", "name", "version", "type", "description", "yieldPercentage", "outputQuantity", "outputUom", "status", "ingredientCount", "stepCount", "createdAt", "updatedAt"})
public record RecipeSummaryResponse( public record RecipeSummaryResponse(
@ -19,8 +19,8 @@ public record RecipeSummaryResponse(
String status, String status,
int ingredientCount, int ingredientCount,
int stepCount, int stepCount,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime updatedAt OffsetDateTime updatedAt
) { ) {
public static RecipeSummaryResponse from(Recipe recipe) { public static RecipeSummaryResponse from(Recipe recipe) {
return new RecipeSummaryResponse( return new RecipeSummaryResponse(

View file

@ -5,7 +5,7 @@ import jakarta.persistence.*;
import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@ -48,10 +48,10 @@ public class UserEntity {
@CreatedDate @CreatedDate
@Column(name = "created_at", nullable = false, updatable = false) @Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt; private OffsetDateTime createdAt;
@Column(name = "last_login") @Column(name = "last_login")
private LocalDateTime lastLogin; private OffsetDateTime lastLogin;
// JPA requires no-arg constructor // JPA requires no-arg constructor
protected UserEntity() { protected UserEntity() {
@ -65,8 +65,8 @@ public class UserEntity {
Set<RoleEntity> roles, Set<RoleEntity> roles,
String branchId, String branchId,
UserStatus status, UserStatus status,
LocalDateTime createdAt, OffsetDateTime createdAt,
LocalDateTime lastLogin OffsetDateTime lastLogin
) { ) {
this.id = id; this.id = id;
this.username = username; this.username = username;
@ -136,19 +136,19 @@ public class UserEntity {
this.status = status; this.status = status;
} }
public LocalDateTime getCreatedAt() { public OffsetDateTime getCreatedAt() {
return createdAt; return createdAt;
} }
public void setCreatedAt(LocalDateTime createdAt) { public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt; this.createdAt = createdAt;
} }
public LocalDateTime getLastLogin() { public OffsetDateTime getLastLogin() {
return lastLogin; return lastLogin;
} }
public void setLastLogin(LocalDateTime lastLogin) { public void setLastLogin(OffsetDateTime lastLogin) {
this.lastLogin = lastLogin; this.lastLogin = lastLogin;
} }
} }

View file

@ -2,7 +2,8 @@ package de.effigenix.infrastructure.usermanagement.web.dto;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List; import java.util.List;
/** /**
@ -23,7 +24,7 @@ public record ErrorResponse(
int status, int status,
@Schema(description = "Timestamp when error occurred") @Schema(description = "Timestamp when error occurred")
LocalDateTime timestamp, OffsetDateTime timestamp,
@Schema(description = "Request path where error occurred", example = "/api/users/user-123") @Schema(description = "Request path where error occurred", example = "/api/users/user-123")
String path, String path,
@ -44,7 +45,7 @@ public record ErrorResponse(
code, code,
message, message,
status, status,
LocalDateTime.now(), OffsetDateTime.now(ZoneOffset.UTC),
path, path,
null null
); );
@ -63,7 +64,7 @@ public record ErrorResponse(
"VALIDATION_ERROR", "VALIDATION_ERROR",
message, message,
status, status,
LocalDateTime.now(), OffsetDateTime.now(ZoneOffset.UTC),
path, path,
validationErrors validationErrors
); );

View file

@ -3,7 +3,7 @@ package de.effigenix.infrastructure.usermanagement.web.dto;
import de.effigenix.application.usermanagement.dto.SessionToken; import de.effigenix.application.usermanagement.dto.SessionToken;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
/** /**
* Response DTO for successful login. * Response DTO for successful login.
@ -25,7 +25,7 @@ public record LoginResponse(
long expiresIn, long expiresIn,
@Schema(description = "Token expiration timestamp") @Schema(description = "Token expiration timestamp")
LocalDateTime expiresAt, OffsetDateTime expiresAt,
@Schema(description = "Refresh token for obtaining new access token") @Schema(description = "Refresh token for obtaining new access token")
String refreshToken String refreshToken

View file

@ -69,7 +69,14 @@ public final class Quantity {
} }
/** /**
* Reconstitutes a Quantity from persistence. No validation. * Reconstitutes a simple Quantity from persistence. No validation.
*/
public static Quantity reconstitute(BigDecimal amount, UnitOfMeasure uom) {
return new Quantity(amount, uom, null, null);
}
/**
* Reconstitutes a dual Quantity (catch-weight) from persistence. No validation.
*/ */
public static Quantity reconstitute(BigDecimal amount, UnitOfMeasure uom, public static Quantity reconstitute(BigDecimal amount, UnitOfMeasure uom,
BigDecimal secondaryAmount, UnitOfMeasure secondaryUom) { BigDecimal secondaryAmount, UnitOfMeasure secondaryUom) {

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="016-create-batch-number-sequences-table" author="effigenix">
<createTable tableName="batch_number_sequences">
<column name="production_date" type="DATE">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="last_sequence" type="INT" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="017-timestamps-to-timestamptz" author="effigenix">
<comment>Migrate all TIMESTAMP columns to TIMESTAMP WITH TIME ZONE for consistent timezone handling</comment>
<!-- users -->
<sql>ALTER TABLE users ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE;</sql>
<sql>ALTER TABLE users ALTER COLUMN last_login TYPE TIMESTAMP WITH TIME ZONE;</sql>
<!-- audit_logs -->
<sql>ALTER TABLE audit_logs ALTER COLUMN timestamp TYPE TIMESTAMP WITH TIME ZONE;</sql>
<sql>ALTER TABLE audit_logs ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE;</sql>
<!-- articles -->
<sql>ALTER TABLE articles ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE;</sql>
<sql>ALTER TABLE articles ALTER COLUMN updated_at TYPE TIMESTAMP WITH TIME ZONE;</sql>
<!-- suppliers -->
<sql>ALTER TABLE suppliers ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE;</sql>
<sql>ALTER TABLE suppliers ALTER COLUMN updated_at TYPE TIMESTAMP WITH TIME ZONE;</sql>
<!-- customers -->
<sql>ALTER TABLE customers ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE;</sql>
<sql>ALTER TABLE customers ALTER COLUMN updated_at TYPE TIMESTAMP WITH TIME ZONE;</sql>
<!-- recipes -->
<sql>ALTER TABLE recipes ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE;</sql>
<sql>ALTER TABLE recipes ALTER COLUMN updated_at TYPE TIMESTAMP WITH TIME ZONE;</sql>
</changeSet>
</databaseChangeLog>

View file

@ -20,5 +20,7 @@
<include file="db/changelog/changes/013-create-stock-schema.xml"/> <include file="db/changelog/changes/013-create-stock-schema.xml"/>
<include file="db/changelog/changes/014-create-stock-batches-table.xml"/> <include file="db/changelog/changes/014-create-stock-batches-table.xml"/>
<include file="db/changelog/changes/015-create-batches-table.xml"/> <include file="db/changelog/changes/015-create-batches-table.xml"/>
<include file="db/changelog/changes/016-create-batch-number-sequences-table.xml"/>
<include file="db/changelog/changes/017-timestamps-to-timestamptz.xml"/>
</databaseChangeLog> </databaseChangeLog>

View file

@ -119,7 +119,7 @@ class AddStockBatchTest {
List.of(StockBatch.reconstitute( List.of(StockBatch.reconstitute(
StockBatchId.generate(), StockBatchId.generate(),
new BatchReference("BATCH-001", BatchType.PRODUCED), new BatchReference("BATCH-001", BatchType.PRODUCED),
Quantity.reconstitute(new BigDecimal("5"), UnitOfMeasure.KILOGRAM, null, null), Quantity.reconstitute(new BigDecimal("5"), UnitOfMeasure.KILOGRAM),
LocalDate.of(2026, 12, 31), LocalDate.of(2026, 12, 31),
StockBatchStatus.AVAILABLE, StockBatchStatus.AVAILABLE,
Instant.now() Instant.now()

View file

@ -43,7 +43,7 @@ class RemoveStockBatchTest {
var batch = StockBatch.reconstitute( var batch = StockBatch.reconstitute(
batchId, batchId,
new BatchReference("BATCH-001", BatchType.PRODUCED), new BatchReference("BATCH-001", BatchType.PRODUCED),
Quantity.reconstitute(new BigDecimal("10"), UnitOfMeasure.KILOGRAM, null, null), Quantity.reconstitute(new BigDecimal("10"), UnitOfMeasure.KILOGRAM),
LocalDate.of(2026, 12, 31), LocalDate.of(2026, 12, 31),
StockBatchStatus.AVAILABLE, StockBatchStatus.AVAILABLE,
Instant.now() Instant.now()

View file

@ -15,7 +15,8 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -61,7 +62,7 @@ class ActivateRecipeTest {
null, new YieldPercentage(85), 14, null, new YieldPercentage(85), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(), RecipeStatus.ACTIVE, List.of(), List.of(),
LocalDateTime.now(), LocalDateTime.now() OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
); );
} }

View file

@ -15,7 +15,8 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -45,7 +46,7 @@ class ArchiveRecipeTest {
null, new YieldPercentage(85), 14, null, new YieldPercentage(85), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(), RecipeStatus.ACTIVE, List.of(), List.of(),
LocalDateTime.now(), LocalDateTime.now() OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
); );
} }
@ -118,7 +119,7 @@ class ArchiveRecipeTest {
null, new YieldPercentage(85), 14, null, new YieldPercentage(85), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ARCHIVED, List.of(), List.of(), RecipeStatus.ARCHIVED, List.of(), List.of(),
LocalDateTime.now(), LocalDateTime.now() OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
); );
when(authPort.can(performedBy, ProductionAction.RECIPE_WRITE)).thenReturn(true); when(authPort.can(performedBy, ProductionAction.RECIPE_WRITE)).thenReturn(true);
when(recipeRepository.findById(RecipeId.of("recipe-2"))).thenReturn(Result.success(Optional.of(recipe))); when(recipeRepository.findById(RecipeId.of("recipe-2"))).thenReturn(Result.success(Optional.of(recipe)));

View file

@ -14,7 +14,8 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -43,7 +44,7 @@ class GetRecipeTest {
"Beschreibung", new YieldPercentage(85), 14, "Beschreibung", new YieldPercentage(85), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(), RecipeStatus.ACTIVE, List.of(), List.of(),
LocalDateTime.now(), LocalDateTime.now() OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
); );
} }

View file

@ -14,7 +14,8 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List; import java.util.List;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -42,7 +43,7 @@ class ListRecipesTest {
null, new YieldPercentage(85), 14, null, new YieldPercentage(85), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
status, List.of(), List.of(), status, List.of(), List.of(),
LocalDateTime.now(), LocalDateTime.now() OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
); );
} }

View file

@ -17,7 +17,8 @@ import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -57,7 +58,7 @@ class PlanBatchTest {
null, new YieldPercentage(85), 14, null, new YieldPercentage(85), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(), RecipeStatus.ACTIVE, List.of(), List.of(),
LocalDateTime.now(), LocalDateTime.now() OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
); );
} }
@ -67,7 +68,7 @@ class PlanBatchTest {
null, new YieldPercentage(85), 14, null, new YieldPercentage(85), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.DRAFT, List.of(), List.of(), RecipeStatus.DRAFT, List.of(), List.of(),
LocalDateTime.now(), LocalDateTime.now() OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC)
); );
} }

View file

@ -13,7 +13,8 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -48,7 +49,7 @@ class RecipeCycleCheckerTest {
null, new YieldPercentage(100), 14, null, new YieldPercentage(100), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.DRAFT, ingredients, List.of(), RecipeStatus.DRAFT, ingredients, List.of(),
LocalDateTime.now(), LocalDateTime.now()); OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC));
} }
private Recipe recipeWithoutSubRecipes(String id) { private Recipe recipeWithoutSubRecipes(String id) {
@ -145,7 +146,7 @@ class RecipeCycleCheckerTest {
null, new YieldPercentage(100), 14, null, new YieldPercentage(100), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.DRAFT, ingredients, List.of(), RecipeStatus.DRAFT, ingredients, List.of(),
LocalDateTime.now(), LocalDateTime.now()); OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC));
when(recipeRepository.findById(RecipeId.of("B"))).thenReturn(Result.success(Optional.of(recipeB))); when(recipeRepository.findById(RecipeId.of("B"))).thenReturn(Result.success(Optional.of(recipeB)));
when(recipeRepository.findById(RecipeId.of("C"))).thenReturn(Result.success(Optional.of(recipeWithoutSubRecipes("C")))); when(recipeRepository.findById(RecipeId.of("C"))).thenReturn(Result.success(Optional.of(recipeWithoutSubRecipes("C"))));

View file

@ -13,7 +13,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashSet; import java.util.HashSet;
import java.util.Optional; import java.util.Optional;
@ -42,7 +43,7 @@ class AssignRoleTest {
testUser = User.reconstitute( testUser = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com", UserId.of("user-1"), "john.doe", "john@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"), new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.ACTIVE, LocalDateTime.now(), null new HashSet<>(), "branch-1", UserStatus.ACTIVE, OffsetDateTime.now(ZoneOffset.UTC), null
); );
workerRole = Role.reconstitute(RoleId.generate(), RoleName.PRODUCTION_WORKER, new HashSet<>(), "Production Worker"); workerRole = Role.reconstitute(RoleId.generate(), RoleName.PRODUCTION_WORKER, new HashSet<>(), "Production Worker");
} }

View file

@ -13,7 +13,8 @@ import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashSet; import java.util.HashSet;
import java.util.Optional; import java.util.Optional;
@ -45,10 +46,10 @@ class AuthenticateUserTest {
testUser = User.reconstitute( testUser = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com", validPasswordHash, UserId.of("user-1"), "john.doe", "john@example.com", validPasswordHash,
new HashSet<>(), "branch-1", UserStatus.ACTIVE, LocalDateTime.now(), null new HashSet<>(), "branch-1", UserStatus.ACTIVE, OffsetDateTime.now(ZoneOffset.UTC), null
); );
sessionToken = new SessionToken("jwt-token", "Bearer", 3600L, LocalDateTime.now().plusSeconds(3600), "refresh-token"); sessionToken = new SessionToken("jwt-token", "Bearer", 3600L, OffsetDateTime.now(ZoneOffset.UTC).plusSeconds(3600), "refresh-token");
} }
@Test @Test
@ -84,7 +85,7 @@ class AuthenticateUserTest {
void should_FailWithLockedUser_When_UserStatusIsLocked() { void should_FailWithLockedUser_When_UserStatusIsLocked() {
User lockedUser = User.reconstitute( User lockedUser = User.reconstitute(
UserId.of("user-2"), "john.doe", "john@example.com", validPasswordHash, UserId.of("user-2"), "john.doe", "john@example.com", validPasswordHash,
new HashSet<>(), "branch-1", UserStatus.LOCKED, LocalDateTime.now(), null new HashSet<>(), "branch-1", UserStatus.LOCKED, OffsetDateTime.now(ZoneOffset.UTC), null
); );
when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(lockedUser))); when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(lockedUser)));
@ -100,7 +101,7 @@ class AuthenticateUserTest {
void should_FailWithInactiveUser_When_UserStatusIsInactive() { void should_FailWithInactiveUser_When_UserStatusIsInactive() {
User inactiveUser = User.reconstitute( User inactiveUser = User.reconstitute(
UserId.of("user-3"), "john.doe", "john@example.com", validPasswordHash, UserId.of("user-3"), "john.doe", "john@example.com", validPasswordHash,
new HashSet<>(), "branch-1", UserStatus.INACTIVE, LocalDateTime.now(), null new HashSet<>(), "branch-1", UserStatus.INACTIVE, OffsetDateTime.now(ZoneOffset.UTC), null
); );
when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(inactiveUser))); when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(inactiveUser)));
@ -143,7 +144,7 @@ class AuthenticateUserTest {
void should_NotCreateSession_When_UserLocked() { void should_NotCreateSession_When_UserLocked() {
User lockedUser = User.reconstitute( User lockedUser = User.reconstitute(
UserId.of("user-4"), "john.doe", "john@example.com", validPasswordHash, UserId.of("user-4"), "john.doe", "john@example.com", validPasswordHash,
new HashSet<>(), "branch-1", UserStatus.LOCKED, LocalDateTime.now(), null new HashSet<>(), "branch-1", UserStatus.LOCKED, OffsetDateTime.now(ZoneOffset.UTC), null
); );
when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(lockedUser))); when(userRepository.findByUsername("john.doe")).thenReturn(Result.success(Optional.of(lockedUser)));

View file

@ -12,7 +12,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashSet; import java.util.HashSet;
import java.util.Optional; import java.util.Optional;
@ -44,7 +45,7 @@ class ChangePasswordTest {
testUser = User.reconstitute( testUser = User.reconstitute(
UserId.of("user-123"), "john.doe", "john@example.com", oldPasswordHash, UserId.of("user-123"), "john.doe", "john@example.com", oldPasswordHash,
new HashSet<>(), "branch-1", UserStatus.ACTIVE, LocalDateTime.now(), null new HashSet<>(), "branch-1", UserStatus.ACTIVE, OffsetDateTime.now(ZoneOffset.UTC), null
); );
validCommand = new ChangePasswordCommand("user-123", "OldPassword123!", "NewPassword456!"); validCommand = new ChangePasswordCommand("user-123", "OldPassword123!", "NewPassword456!");

View file

@ -12,7 +12,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashSet; import java.util.HashSet;
import java.util.Optional; import java.util.Optional;
@ -37,7 +38,7 @@ class GetUserTest {
testUser = User.reconstitute( testUser = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com", UserId.of("user-1"), "john.doe", "john@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"), new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.ACTIVE, LocalDateTime.now(), null new HashSet<>(), "branch-1", UserStatus.ACTIVE, OffsetDateTime.now(ZoneOffset.UTC), null
); );
} }

View file

@ -13,7 +13,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -39,12 +40,12 @@ class ListUsersTest {
user1 = User.reconstitute( user1 = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com", UserId.of("user-1"), "john.doe", "john@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"), new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.ACTIVE, LocalDateTime.now(), null new HashSet<>(), "branch-1", UserStatus.ACTIVE, OffsetDateTime.now(ZoneOffset.UTC), null
); );
user2 = User.reconstitute( user2 = User.reconstitute(
UserId.of("user-2"), "jane.doe", "jane@example.com", UserId.of("user-2"), "jane.doe", "jane@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"), new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.ACTIVE, LocalDateTime.now(), null new HashSet<>(), "branch-1", UserStatus.ACTIVE, OffsetDateTime.now(ZoneOffset.UTC), null
); );
} }

View file

@ -13,7 +13,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashSet; import java.util.HashSet;
import java.util.Optional; import java.util.Optional;
@ -40,7 +41,7 @@ class LockUserTest {
activeUser = User.reconstitute( activeUser = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com", UserId.of("user-1"), "john.doe", "john@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"), new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.ACTIVE, LocalDateTime.now(), null new HashSet<>(), "branch-1", UserStatus.ACTIVE, OffsetDateTime.now(ZoneOffset.UTC), null
); );
} }
@ -100,7 +101,7 @@ class LockUserTest {
User lockedUser = User.reconstitute( User lockedUser = User.reconstitute(
UserId.of("user-2"), "jane.doe", "jane@example.com", UserId.of("user-2"), "jane.doe", "jane@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"), new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.LOCKED, LocalDateTime.now(), null new HashSet<>(), "branch-1", UserStatus.LOCKED, OffsetDateTime.now(ZoneOffset.UTC), null
); );
when(authPort.can(performedBy, UserManagementAction.USER_LOCK)).thenReturn(true); when(authPort.can(performedBy, UserManagementAction.USER_LOCK)).thenReturn(true);
when(userRepository.findById(UserId.of("user-2"))).thenReturn(Result.success(Optional.of(lockedUser))); when(userRepository.findById(UserId.of("user-2"))).thenReturn(Result.success(Optional.of(lockedUser)));
@ -118,7 +119,7 @@ class LockUserTest {
User inactiveUser = User.reconstitute( User inactiveUser = User.reconstitute(
UserId.of("user-3"), "bob", "bob@example.com", UserId.of("user-3"), "bob", "bob@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"), new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.INACTIVE, LocalDateTime.now(), null new HashSet<>(), "branch-1", UserStatus.INACTIVE, OffsetDateTime.now(ZoneOffset.UTC), null
); );
when(authPort.can(performedBy, UserManagementAction.USER_LOCK)).thenReturn(true); when(authPort.can(performedBy, UserManagementAction.USER_LOCK)).thenReturn(true);
when(userRepository.findById(UserId.of("user-3"))).thenReturn(Result.success(Optional.of(inactiveUser))); when(userRepository.findById(UserId.of("user-3"))).thenReturn(Result.success(Optional.of(inactiveUser)));

View file

@ -13,7 +13,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashSet; import java.util.HashSet;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@ -44,7 +45,7 @@ class RemoveRoleTest {
userWithRole = User.reconstitute( userWithRole = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com", UserId.of("user-1"), "john.doe", "john@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"), new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(Set.of(workerRole)), "branch-1", UserStatus.ACTIVE, LocalDateTime.now(), null new HashSet<>(Set.of(workerRole)), "branch-1", UserStatus.ACTIVE, OffsetDateTime.now(ZoneOffset.UTC), null
); );
} }

View file

@ -13,7 +13,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashSet; import java.util.HashSet;
import java.util.Optional; import java.util.Optional;
@ -40,7 +41,7 @@ class UnlockUserTest {
lockedUser = User.reconstitute( lockedUser = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com", UserId.of("user-1"), "john.doe", "john@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"), new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.LOCKED, LocalDateTime.now(), null new HashSet<>(), "branch-1", UserStatus.LOCKED, OffsetDateTime.now(ZoneOffset.UTC), null
); );
} }
@ -100,7 +101,7 @@ class UnlockUserTest {
User activeUser = User.reconstitute( User activeUser = User.reconstitute(
UserId.of("user-2"), "jane.doe", "jane@example.com", UserId.of("user-2"), "jane.doe", "jane@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"), new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.ACTIVE, LocalDateTime.now(), null new HashSet<>(), "branch-1", UserStatus.ACTIVE, OffsetDateTime.now(ZoneOffset.UTC), null
); );
when(authPort.can(performedBy, UserManagementAction.USER_UNLOCK)).thenReturn(true); when(authPort.can(performedBy, UserManagementAction.USER_UNLOCK)).thenReturn(true);
when(userRepository.findById(UserId.of("user-2"))).thenReturn(Result.success(Optional.of(activeUser))); when(userRepository.findById(UserId.of("user-2"))).thenReturn(Result.success(Optional.of(activeUser)));
@ -118,7 +119,7 @@ class UnlockUserTest {
User inactiveUser = User.reconstitute( User inactiveUser = User.reconstitute(
UserId.of("user-3"), "bob", "bob@example.com", UserId.of("user-3"), "bob", "bob@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"), new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.INACTIVE, LocalDateTime.now(), null new HashSet<>(), "branch-1", UserStatus.INACTIVE, OffsetDateTime.now(ZoneOffset.UTC), null
); );
when(authPort.can(performedBy, UserManagementAction.USER_UNLOCK)).thenReturn(true); when(authPort.can(performedBy, UserManagementAction.USER_UNLOCK)).thenReturn(true);
when(userRepository.findById(UserId.of("user-3"))).thenReturn(Result.success(Optional.of(inactiveUser))); when(userRepository.findById(UserId.of("user-3"))).thenReturn(Result.success(Optional.of(inactiveUser)));

View file

@ -13,7 +13,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashSet; import java.util.HashSet;
import java.util.Optional; import java.util.Optional;
@ -40,7 +41,7 @@ class UpdateUserTest {
testUser = User.reconstitute( testUser = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com", UserId.of("user-1"), "john.doe", "john@example.com",
new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"), new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"),
new HashSet<>(), "branch-1", UserStatus.ACTIVE, LocalDateTime.now(), null new HashSet<>(), "branch-1", UserStatus.ACTIVE, OffsetDateTime.now(ZoneOffset.UTC), null
); );
} }

View file

@ -266,7 +266,7 @@ class StockTest {
var id = StockId.generate(); var id = StockId.generate();
var articleId = ArticleId.of("article-1"); var articleId = ArticleId.of("article-1");
var locationId = StorageLocationId.of("location-1"); var locationId = StorageLocationId.of("location-1");
var quantity = Quantity.reconstitute(new BigDecimal("10"), UnitOfMeasure.KILOGRAM, null, null); var quantity = Quantity.reconstitute(new BigDecimal("10"), UnitOfMeasure.KILOGRAM);
var minimumLevel = new MinimumLevel(quantity); var minimumLevel = new MinimumLevel(quantity);
var minimumShelfLife = new MinimumShelfLife(30); var minimumShelfLife = new MinimumShelfLife(30);
@ -606,7 +606,7 @@ class StockTest {
var batch = StockBatch.reconstitute( var batch = StockBatch.reconstitute(
StockBatchId.generate(), StockBatchId.generate(),
new BatchReference("BATCH-001", BatchType.PRODUCED), new BatchReference("BATCH-001", BatchType.PRODUCED),
Quantity.reconstitute(new BigDecimal(amount), uom, null, null), Quantity.reconstitute(new BigDecimal(amount), uom),
LocalDate.of(2026, 12, 31), LocalDate.of(2026, 12, 31),
status, status,
Instant.now() Instant.now()

View file

@ -8,7 +8,8 @@ import org.junit.jupiter.api.Test;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -187,8 +188,8 @@ class BatchTest {
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
PRODUCTION_DATE, PRODUCTION_DATE,
BEST_BEFORE_DATE, BEST_BEFORE_DATE,
LocalDateTime.now(), OffsetDateTime.now(ZoneOffset.UTC),
LocalDateTime.now() OffsetDateTime.now(ZoneOffset.UTC)
); );
assertThat(batch.id().value()).isEqualTo("batch-1"); assertThat(batch.id().value()).isEqualTo("batch-1");

View file

@ -239,7 +239,7 @@ class RecipeTest {
null, new YieldPercentage(85), 14, null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(), RecipeStatus.ACTIVE, List.of(), List.of(),
java.time.LocalDateTime.now(), java.time.LocalDateTime.now() java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
); );
var result = recipe.addIngredient(validIngredientDraft(1)); var result = recipe.addIngredient(validIngredientDraft(1));
@ -322,7 +322,7 @@ class RecipeTest {
null, new YieldPercentage(85), 14, null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(), RecipeStatus.ACTIVE, List.of(), List.of(),
java.time.LocalDateTime.now(), java.time.LocalDateTime.now() java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
); );
var result = recipe.removeIngredient(IngredientId.generate()); var result = recipe.removeIngredient(IngredientId.generate());
@ -376,7 +376,7 @@ class RecipeTest {
null, new YieldPercentage(85), 14, null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(), RecipeStatus.ACTIVE, List.of(), List.of(),
java.time.LocalDateTime.now(), java.time.LocalDateTime.now() java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
); );
var result = recipe.addProductionStep(new ProductionStepDraft(1, "Mischen", null, null)); var result = recipe.addProductionStep(new ProductionStepDraft(1, "Mischen", null, null));
@ -445,7 +445,7 @@ class RecipeTest {
null, new YieldPercentage(85), 14, null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(), RecipeStatus.ACTIVE, List.of(), List.of(),
java.time.LocalDateTime.now(), java.time.LocalDateTime.now() java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
); );
var result = recipe.removeProductionStep(1); var result = recipe.removeProductionStep(1);
@ -507,7 +507,7 @@ class RecipeTest {
null, new YieldPercentage(85), 14, null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(), RecipeStatus.ACTIVE, List.of(), List.of(),
java.time.LocalDateTime.now(), java.time.LocalDateTime.now() java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
); );
var result = recipe.activate(); var result = recipe.activate();
@ -528,7 +528,7 @@ class RecipeTest {
null, new YieldPercentage(85), 14, null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ARCHIVED, List.of(), List.of(), RecipeStatus.ARCHIVED, List.of(), List.of(),
java.time.LocalDateTime.now(), java.time.LocalDateTime.now() java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
); );
var result = recipe.activate(); var result = recipe.activate();
@ -554,7 +554,7 @@ class RecipeTest {
null, new YieldPercentage(85), 14, null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ACTIVE, List.of(), List.of(), RecipeStatus.ACTIVE, List.of(), List.of(),
java.time.LocalDateTime.now(), java.time.LocalDateTime.now() java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
); );
var updatedBefore = recipe.updatedAt(); var updatedBefore = recipe.updatedAt();
@ -588,7 +588,7 @@ class RecipeTest {
null, new YieldPercentage(85), 14, null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(), Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.ARCHIVED, List.of(), List.of(), RecipeStatus.ARCHIVED, List.of(), List.of(),
java.time.LocalDateTime.now(), java.time.LocalDateTime.now() java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC), java.time.OffsetDateTime.now(java.time.ZoneOffset.UTC)
); );
var result = recipe.archive(); var result = recipe.archive();

View file

@ -7,7 +7,8 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.*; import java.util.*;
import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.*;
@ -25,7 +26,7 @@ class UserTest {
private PasswordHash passwordHash; private PasswordHash passwordHash;
private Set<Role> roles; private Set<Role> roles;
private String branchId; private String branchId;
private LocalDateTime createdAt; private OffsetDateTime createdAt;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
@ -35,7 +36,7 @@ class UserTest {
passwordHash = new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW"); passwordHash = new PasswordHash("$2a$12$R9h/cIPz0gi.URNN3kh2OPST9EBwVeL00lzQRYe3z08MZx3e8YCWW");
roles = new HashSet<>(); roles = new HashSet<>();
branchId = "branch-1"; branchId = "branch-1";
createdAt = LocalDateTime.now(); createdAt = OffsetDateTime.now(ZoneOffset.UTC);
} }
@Test @Test
@ -56,9 +57,9 @@ class UserTest {
@Test @Test
@DisplayName("should_SetDefaultCreatedAtToNow_When_NullProvidedForCreatedAt") @DisplayName("should_SetDefaultCreatedAtToNow_When_NullProvidedForCreatedAt")
void should_SetDefaultCreatedAtToNow_When_NullProvidedForCreatedAt() { void should_SetDefaultCreatedAtToNow_When_NullProvidedForCreatedAt() {
LocalDateTime before = LocalDateTime.now(); OffsetDateTime before = OffsetDateTime.now(ZoneOffset.UTC);
User user = User.reconstitute(userId, username, email, passwordHash, roles, branchId, UserStatus.ACTIVE, null, null); User user = User.reconstitute(userId, username, email, passwordHash, roles, branchId, UserStatus.ACTIVE, null, null);
LocalDateTime after = LocalDateTime.now(); OffsetDateTime after = OffsetDateTime.now(ZoneOffset.UTC);
assertThat(user.createdAt()).isNotNull(); assertThat(user.createdAt()).isNotNull();
assertThat(user.createdAt()).isBetween(before, after); assertThat(user.createdAt()).isBetween(before, after);
@ -129,7 +130,7 @@ class UserTest {
@DisplayName("should_ReturnNewUserWithLastLogin_When_WithLastLoginCalled") @DisplayName("should_ReturnNewUserWithLastLogin_When_WithLastLoginCalled")
void should_ReturnNewUserWithLastLogin_When_WithLastLoginCalled() { void should_ReturnNewUserWithLastLogin_When_WithLastLoginCalled() {
User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue(); User user = User.create(username, email, passwordHash, roles, branchId).unsafeGetValue();
LocalDateTime now = LocalDateTime.now(); OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
User updated = user.withLastLogin(now).unsafeGetValue(); User updated = user.withLastLogin(now).unsafeGetValue();

View file

@ -18,7 +18,8 @@ import org.springframework.transaction.annotation.Transactional;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Date; import java.util.Date;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@ -102,7 +103,7 @@ public abstract class AbstractIntegrationTest {
UserEntity user = new UserEntity( UserEntity user = new UserEntity(
UUID.randomUUID().toString(), username, email, UUID.randomUUID().toString(), username, email,
BCRYPT_PASS123, roles, BCRYPT_PASS123, roles,
branchId, UserStatus.ACTIVE, LocalDateTime.now(), null); branchId, UserStatus.ACTIVE, OffsetDateTime.now(ZoneOffset.UTC), null);
return userRepository.save(user); return userRepository.save(user);
} }
} }

View file

@ -7,7 +7,8 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@ -29,11 +30,11 @@ class UserMapperTest {
private User domainUser; private User domainUser;
private UserEntity jpaEntity; private UserEntity jpaEntity;
private LocalDateTime createdAt; private OffsetDateTime createdAt;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
createdAt = LocalDateTime.now(); createdAt = OffsetDateTime.now(ZoneOffset.UTC);
// Create JPA entity first // Create JPA entity first
jpaEntity = new UserEntity( jpaEntity = new UserEntity(
@ -112,7 +113,7 @@ class UserMapperTest {
@DisplayName("should_PreserveAllUserFields_When_MappingToEntity") @DisplayName("should_PreserveAllUserFields_When_MappingToEntity")
void should_PreserveAllUserFields_When_MappingToEntity() { void should_PreserveAllUserFields_When_MappingToEntity() {
// Arrange // Arrange
LocalDateTime lastLogin = LocalDateTime.now(); OffsetDateTime lastLogin = OffsetDateTime.now(ZoneOffset.UTC);
UserEntity sourceEntity = new UserEntity( UserEntity sourceEntity = new UserEntity(
"user-456", "user-456",
"jane.smith", "jane.smith",
@ -144,7 +145,7 @@ class UserMapperTest {
@DisplayName("should_PreserveAllEntityFields_When_MappingToDomain") @DisplayName("should_PreserveAllEntityFields_When_MappingToDomain")
void should_PreserveAllEntityFields_When_MappingToDomain() { void should_PreserveAllEntityFields_When_MappingToDomain() {
// Arrange // Arrange
LocalDateTime lastLogin = LocalDateTime.now(); OffsetDateTime lastLogin = OffsetDateTime.now(ZoneOffset.UTC);
UserEntity entityWithLastLogin = new UserEntity( UserEntity entityWithLastLogin = new UserEntity(
"user-789", "user-789",
"bob.jones", "bob.jones",

View file

@ -18,7 +18,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
import java.time.LocalDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@ -327,7 +328,7 @@ class SecurityIntegrationTest extends AbstractIntegrationTest {
.orElseThrow(); .orElseThrow();
assertThat(auditLog.getTimestamp()).isNotNull(); assertThat(auditLog.getTimestamp()).isNotNull();
assertThat(auditLog.getTimestamp()).isAfter(LocalDateTime.now().minusMinutes(1)); assertThat(auditLog.getTimestamp()).isAfter(OffsetDateTime.now(ZoneOffset.UTC).minusMinutes(1));
} }
@Test @Test