1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 06: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 org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Optional;
/**
@ -71,7 +72,7 @@ public class AuthenticateUser {
SessionToken token = sessionManager.createSession(user);
// 5. Update last login timestamp (immutable)
return user.withLastLogin(LocalDateTime.now())
return user.withLastLogin(OffsetDateTime.now(ZoneOffset.UTC))
.flatMap(updated -> userRepository.save(updated)
.mapError(err -> (UserError) new UserError.RepositoryFailure(err.message()))
.map(ignored -> {

View file

@ -1,6 +1,7 @@
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.
@ -9,7 +10,7 @@ public record SessionToken(
String accessToken,
String tokenType,
long expiresIn, // in seconds
LocalDateTime expiresAt,
OffsetDateTime expiresAt,
String refreshToken // for future refresh token support
) {
public static SessionToken create(String accessToken, long expiresInMs, String refreshToken) {
@ -17,7 +18,7 @@ public record SessionToken(
accessToken,
"Bearer",
expiresInMs / 1000, // convert to seconds
LocalDateTime.now().plusSeconds(expiresInMs / 1000),
OffsetDateTime.now(ZoneOffset.UTC).plusSeconds(expiresInMs / 1000),
refreshToken
);
}

View file

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

View file

@ -37,7 +37,7 @@ public record MinimumLevel(Quantity quantity) {
// MinimumLevel erlaubt amount == 0 (kein Mindestbestand)
// Quantity.of() verlangt amount > 0, daher Reconstitute verwenden
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)

View file

@ -108,7 +108,7 @@ public class StockBatch {
return Result.failure(new StockError.NegativeStockNotAllowed());
}
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())) {
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 java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.*;
/**
@ -26,8 +27,8 @@ public class Article {
private final List<SalesUnit> salesUnits;
private ArticleStatus status;
private final Set<SupplierId> supplierReferences;
private final LocalDateTime createdAt;
private LocalDateTime updatedAt;
private final OffsetDateTime createdAt;
private OffsetDateTime updatedAt;
private Article(
ArticleId id,
@ -37,8 +38,8 @@ public class Article {
List<SalesUnit> salesUnits,
ArticleStatus status,
Set<SupplierId> supplierReferences,
LocalDateTime createdAt,
LocalDateTime updatedAt
OffsetDateTime createdAt,
OffsetDateTime updatedAt
) {
this.id = id;
this.name = name;
@ -88,7 +89,7 @@ public class Article {
case Success(var val) -> salesUnit = val;
}
var now = LocalDateTime.now();
var now = OffsetDateTime.now(ZoneOffset.UTC);
return Result.success(new Article(
ArticleId.generate(),
name,
@ -110,8 +111,8 @@ public class Article {
List<SalesUnit> salesUnits,
ArticleStatus status,
Set<SupplierId> supplierReferences,
LocalDateTime createdAt,
LocalDateTime updatedAt
OffsetDateTime createdAt,
OffsetDateTime updatedAt
) {
return new Article(id, name, articleNumber, categoryId, salesUnits, status,
supplierReferences, createdAt, updatedAt);
@ -208,7 +209,7 @@ public class Article {
}
private void touch() {
this.updatedAt = LocalDateTime.now();
this.updatedAt = OffsetDateTime.now(ZoneOffset.UTC);
}
// ==================== Getters ====================
@ -220,8 +221,8 @@ public class Article {
public List<SalesUnit> salesUnits() { return Collections.unmodifiableList(salesUnits); }
public ArticleStatus status() { return status; }
public Set<SupplierId> supplierReferences() { return Collections.unmodifiableSet(supplierReferences); }
public LocalDateTime createdAt() { return createdAt; }
public LocalDateTime updatedAt() { return updatedAt; }
public OffsetDateTime createdAt() { return createdAt; }
public OffsetDateTime updatedAt() { return updatedAt; }
@Override
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 java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.*;
/**
@ -33,8 +34,8 @@ public class Customer {
private FrameContract frameContract;
private final Set<CustomerPreference> preferences;
private CustomerStatus status;
private final LocalDateTime createdAt;
private LocalDateTime updatedAt;
private final OffsetDateTime createdAt;
private OffsetDateTime updatedAt;
private Customer(
CustomerId id,
@ -47,8 +48,8 @@ public class Customer {
FrameContract frameContract,
Set<CustomerPreference> preferences,
CustomerStatus status,
LocalDateTime createdAt,
LocalDateTime updatedAt
OffsetDateTime createdAt,
OffsetDateTime updatedAt
) {
this.id = id;
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(
CustomerId.generate(), name, draft.type(), billingAddress, contactInfo, paymentTerms,
List.of(), null, Set.of(), CustomerStatus.ACTIVE, now, now
@ -118,8 +119,8 @@ public class Customer {
FrameContract frameContract,
Set<CustomerPreference> preferences,
CustomerStatus status,
LocalDateTime createdAt,
LocalDateTime updatedAt
OffsetDateTime createdAt,
OffsetDateTime updatedAt
) {
return new Customer(id, name, type, billingAddress, contactInfo, paymentTerms,
deliveryAddresses, frameContract, preferences, status, createdAt, updatedAt);
@ -235,7 +236,7 @@ public class Customer {
// ==================== Helpers ====================
private void touch() {
this.updatedAt = LocalDateTime.now();
this.updatedAt = OffsetDateTime.now(ZoneOffset.UTC);
}
// ==================== Getters ====================
@ -250,8 +251,8 @@ public class Customer {
public FrameContract frameContract() { return frameContract; }
public Set<CustomerPreference> preferences() { return Collections.unmodifiableSet(preferences); }
public CustomerStatus status() { return status; }
public LocalDateTime createdAt() { return createdAt; }
public LocalDateTime updatedAt() { return updatedAt; }
public OffsetDateTime createdAt() { return createdAt; }
public OffsetDateTime updatedAt() { return updatedAt; }
@Override
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.PaymentTerms;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -31,8 +32,8 @@ public class Supplier {
private final List<QualityCertificate> certificates;
private SupplierRating rating;
private SupplierStatus status;
private final LocalDateTime createdAt;
private LocalDateTime updatedAt;
private final OffsetDateTime createdAt;
private OffsetDateTime updatedAt;
private Supplier(
SupplierId id,
@ -43,8 +44,8 @@ public class Supplier {
List<QualityCertificate> certificates,
SupplierRating rating,
SupplierStatus status,
LocalDateTime createdAt,
LocalDateTime updatedAt
OffsetDateTime createdAt,
OffsetDateTime updatedAt
) {
this.id = id;
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(
SupplierId.generate(), name, address, contactInfo, paymentTerms,
List.of(), null, SupplierStatus.ACTIVE, now, now
@ -112,8 +113,8 @@ public class Supplier {
List<QualityCertificate> certificates,
SupplierRating rating,
SupplierStatus status,
LocalDateTime createdAt,
LocalDateTime updatedAt
OffsetDateTime createdAt,
OffsetDateTime updatedAt
) {
return new Supplier(id, name, address, contactInfo, paymentTerms,
certificates, rating, status, createdAt, updatedAt);
@ -200,7 +201,7 @@ public class Supplier {
// ==================== Helpers ====================
private void touch() {
this.updatedAt = LocalDateTime.now();
this.updatedAt = OffsetDateTime.now(ZoneOffset.UTC);
}
// ==================== Getters ====================
@ -213,8 +214,8 @@ public class Supplier {
public List<QualityCertificate> certificates() { return Collections.unmodifiableList(certificates); }
public SupplierRating rating() { return rating; }
public SupplierStatus status() { return status; }
public LocalDateTime createdAt() { return createdAt; }
public LocalDateTime updatedAt() { return updatedAt; }
public OffsetDateTime createdAt() { return createdAt; }
public OffsetDateTime updatedAt() { return updatedAt; }
@Override
public boolean equals(Object obj) {

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import de.effigenix.application.usermanagement.AuditEvent;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.List;
/**
@ -34,7 +34,7 @@ public interface AuditLogJpaRepository extends JpaRepository<AuditLogEntity, Str
/**
* 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.

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.ServletRequestAttributes;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.UUID;
/**
@ -52,7 +53,7 @@ public class DatabaseAuditLogger implements AuditLogger {
entityId,
performedBy.value(),
null, // no additional details
LocalDateTime.now(),
OffsetDateTime.now(ZoneOffset.UTC),
getClientIpAddress(),
getUserAgent()
);
@ -75,7 +76,7 @@ public class DatabaseAuditLogger implements AuditLogger {
null, // no entity ID
null, // no actor (e.g., system event)
details,
LocalDateTime.now(),
OffsetDateTime.now(ZoneOffset.UTC),
getClientIpAddress(),
getUserAgent()
);
@ -97,7 +98,7 @@ public class DatabaseAuditLogger implements AuditLogger {
entityId,
performedBy.value(),
details,
LocalDateTime.now(),
OffsetDateTime.now(ZoneOffset.UTC),
getClientIpAddress(),
getUserAgent()
);
@ -119,7 +120,7 @@ public class DatabaseAuditLogger implements AuditLogger {
null, // no entity ID
performedBy.value(),
null, // no additional details
LocalDateTime.now(),
OffsetDateTime.now(ZoneOffset.UTC),
getClientIpAddress(),
getUserAgent()
);

View file

@ -1,14 +1,26 @@
package de.effigenix.infrastructure.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.data.auditing.DateTimeProvider;
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.
* Verwendet OffsetDateTime (UTC) statt LocalDateTime für @CreatedDate/@LastModifiedDate.
*/
@Configuration
@Profile("!no-db")
@EnableJpaAuditing
@EnableJpaAuditing(dateTimeProviderRef = "utcDateTimeProvider")
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) {
var quantity = Quantity.reconstitute(
entity.getMinimumLevelAmount(),
UnitOfMeasure.valueOf(entity.getMinimumLevelUnit()),
null, null
UnitOfMeasure.valueOf(entity.getMinimumLevelUnit())
);
minimumLevel = new MinimumLevel(quantity);
}
@ -87,8 +86,7 @@ public class StockMapper {
new BatchReference(entity.getBatchId(), BatchType.valueOf(entity.getBatchType())),
Quantity.reconstitute(
entity.getQuantityAmount(),
UnitOfMeasure.valueOf(entity.getQuantityUnit()),
null, null
UnitOfMeasure.valueOf(entity.getQuantityUnit())
),
entity.getExpiryDate(),
StockBatchStatus.valueOf(entity.getStatus()),

View file

@ -2,7 +2,7 @@ package de.effigenix.infrastructure.masterdata.persistence.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@ -29,10 +29,10 @@ public class ArticleEntity {
private String status;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
private OffsetDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
private OffsetDateTime updatedAt;
@OneToMany(mappedBy = "article", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
private List<SalesUnitEntity> salesUnits = new ArrayList<>();
@ -45,7 +45,7 @@ public class ArticleEntity {
protected ArticleEntity() {}
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.name = name;
this.articleNumber = articleNumber;
@ -60,8 +60,8 @@ public class ArticleEntity {
public String getArticleNumber() { return articleNumber; }
public String getCategoryId() { return categoryId; }
public String getStatus() { return status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public OffsetDateTime getCreatedAt() { return createdAt; }
public OffsetDateTime getUpdatedAt() { return updatedAt; }
public List<SalesUnitEntity> getSalesUnits() { return salesUnits; }
public Set<String> getSupplierIds() { return supplierIds; }
@ -70,8 +70,8 @@ public class ArticleEntity {
public void setArticleNumber(String articleNumber) { this.articleNumber = articleNumber; }
public void setCategoryId(String categoryId) { this.categoryId = categoryId; }
public void setStatus(String status) { this.status = status; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; }
public void setUpdatedAt(OffsetDateTime updatedAt) { this.updatedAt = updatedAt; }
public void setSalesUnits(List<SalesUnitEntity> salesUnits) { this.salesUnits = salesUnits; }
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 java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@ -56,10 +56,10 @@ public class CustomerEntity {
private String status;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
private OffsetDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
private OffsetDateTime updatedAt;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "delivery_addresses", joinColumns = @JoinColumn(name = "customer_id"))
@ -91,8 +91,8 @@ public class CustomerEntity {
public Integer getPaymentDueDays() { return paymentDueDays; }
public String getPaymentDescription() { return paymentDescription; }
public String getStatus() { return status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public OffsetDateTime getCreatedAt() { return createdAt; }
public OffsetDateTime getUpdatedAt() { return updatedAt; }
public List<DeliveryAddressEmbeddable> getDeliveryAddresses() { return deliveryAddresses; }
public Set<de.effigenix.domain.masterdata.CustomerPreference> getPreferences() { return preferences; }
public FrameContractEntity getFrameContract() { return frameContract; }
@ -111,8 +111,8 @@ public class CustomerEntity {
public void setPaymentDueDays(Integer paymentDueDays) { this.paymentDueDays = paymentDueDays; }
public void setPaymentDescription(String paymentDescription) { this.paymentDescription = paymentDescription; }
public void setStatus(String status) { this.status = status; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; }
public void setUpdatedAt(OffsetDateTime updatedAt) { this.updatedAt = updatedAt; }
public void setDeliveryAddresses(List<DeliveryAddressEmbeddable> deliveryAddresses) { this.deliveryAddresses = deliveryAddresses; }
public void setPreferences(Set<de.effigenix.domain.masterdata.CustomerPreference> preferences) { this.preferences = preferences; }
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 java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
@ -60,10 +60,10 @@ public class SupplierEntity {
private String status;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
private OffsetDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
private OffsetDateTime updatedAt;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "quality_certificates", joinColumns = @JoinColumn(name = "supplier_id"))
@ -87,8 +87,8 @@ public class SupplierEntity {
public Integer getDeliveryScore() { return deliveryScore; }
public Integer getPriceScore() { return priceScore; }
public String getStatus() { return status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public OffsetDateTime getCreatedAt() { return createdAt; }
public OffsetDateTime getUpdatedAt() { return updatedAt; }
public List<QualityCertificateEmbeddable> getCertificates() { return certificates; }
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 setPriceScore(Integer priceScore) { this.priceScore = priceScore; }
public void setStatus(String status) { this.status = status; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; }
public void setUpdatedAt(OffsetDateTime updatedAt) { this.updatedAt = updatedAt; }
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 io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.List;
@Schema(requiredProperties = {"id", "name", "articleNumber", "categoryId", "salesUnits", "status", "supplierIds", "createdAt", "updatedAt"})
@ -16,8 +16,8 @@ public record ArticleResponse(
List<SalesUnitResponse> salesUnits,
String status,
List<String> supplierIds,
LocalDateTime createdAt,
LocalDateTime updatedAt
OffsetDateTime createdAt,
OffsetDateTime updatedAt
) {
public static ArticleResponse from(Article article) {
return new ArticleResponse(

View file

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

View file

@ -3,7 +3,7 @@ package de.effigenix.infrastructure.masterdata.web.dto;
import de.effigenix.domain.masterdata.Supplier;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.List;
@Schema(requiredProperties = {"id", "name", "contactInfo", "certificates", "status", "createdAt", "updatedAt"})
@ -16,8 +16,8 @@ public record SupplierResponse(
List<QualityCertificateResponse> certificates,
@Schema(nullable = true) SupplierRatingResponse rating,
String status,
LocalDateTime createdAt,
LocalDateTime updatedAt
OffsetDateTime createdAt,
OffsetDateTime updatedAt
) {
public static SupplierResponse from(Supplier supplier) {
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.BatchNumber;
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 org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
@ -14,17 +16,25 @@ import java.time.LocalDate;
@Profile("!no-db")
public class JpaBatchNumberGenerator implements BatchNumberGenerator {
private final BatchJpaRepository batchJpaRepository;
private final BatchNumberSequenceJpaRepository sequenceRepository;
public JpaBatchNumberGenerator(BatchJpaRepository batchJpaRepository) {
this.batchJpaRepository = batchJpaRepository;
public JpaBatchNumberGenerator(BatchNumberSequenceJpaRepository sequenceRepository) {
this.sequenceRepository = sequenceRepository;
}
@Override
@Transactional
public Result<BatchError, BatchNumber> generateNext(LocalDate date) {
try {
int count = batchJpaRepository.countByProductionDate(date);
int nextSequence = count + 1;
var sequence = sequenceRepository.findByProductionDate(date);
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) {
return Result.failure(new BatchError.ValidationFailure(
"Maximum batch number sequence (999) reached for date " + date));

View file

@ -4,7 +4,7 @@ import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
@Entity
@Table(name = "batches")
@ -36,10 +36,10 @@ public class BatchEntity {
private LocalDate bestBeforeDate;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
private OffsetDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
private OffsetDateTime updatedAt;
protected BatchEntity() {}
@ -52,8 +52,8 @@ public class BatchEntity {
String plannedQuantityUnit,
LocalDate productionDate,
LocalDate bestBeforeDate,
LocalDateTime createdAt,
LocalDateTime updatedAt
OffsetDateTime createdAt,
OffsetDateTime updatedAt
) {
this.id = id;
this.batchNumber = batchNumber;
@ -75,6 +75,6 @@ public class BatchEntity {
public String getPlannedQuantityUnit() { return plannedQuantityUnit; }
public LocalDate getProductionDate() { return productionDate; }
public LocalDate getBestBeforeDate() { return bestBeforeDate; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public OffsetDateTime getCreatedAt() { return createdAt; }
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 java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
@ -44,10 +44,10 @@ public class RecipeEntity {
private String status;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
private OffsetDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
private OffsetDateTime updatedAt;
@OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
@OrderBy("position ASC")
@ -61,7 +61,7 @@ public class RecipeEntity {
public RecipeEntity(String id, String name, int version, String type, String description,
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.name = name;
this.version = version;
@ -86,8 +86,8 @@ public class RecipeEntity {
public BigDecimal getOutputQuantity() { return outputQuantity; }
public String getOutputUom() { return outputUom; }
public String getStatus() { return status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public OffsetDateTime getCreatedAt() { return createdAt; }
public OffsetDateTime getUpdatedAt() { return updatedAt; }
public List<IngredientEntity> getIngredients() { return ingredients; }
public List<ProductionStepEntity> getProductionSteps() { return productionSteps; }
@ -101,8 +101,8 @@ public class RecipeEntity {
public void setOutputQuantity(BigDecimal outputQuantity) { this.outputQuantity = outputQuantity; }
public void setOutputUom(String outputUom) { this.outputUom = outputUom; }
public void setStatus(String status) { this.status = status; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; }
public void setUpdatedAt(OffsetDateTime updatedAt) { this.updatedAt = updatedAt; }
public void setIngredients(List<IngredientEntity> ingredients) { this.ingredients = ingredients; }
public void setProductionSteps(List<ProductionStepEntity> productionSteps) { this.productionSteps = productionSteps; }
}

View file

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

View file

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

View file

@ -2,12 +2,6 @@ package de.effigenix.infrastructure.production.persistence.repository;
import de.effigenix.infrastructure.production.persistence.entity.BatchEntity;
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> {
@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 java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
public record BatchResponse(
String id,
@ -14,8 +14,8 @@ public record BatchResponse(
String plannedQuantityUnit,
LocalDate productionDate,
LocalDate bestBeforeDate,
LocalDateTime createdAt,
LocalDateTime updatedAt
OffsetDateTime createdAt,
OffsetDateTime updatedAt
) {
public static BatchResponse from(Batch batch) {
return new BatchResponse(

View file

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

View file

@ -3,7 +3,7 @@ package de.effigenix.infrastructure.production.web.dto;
import de.effigenix.domain.production.Recipe;
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"})
public record RecipeSummaryResponse(
@ -19,8 +19,8 @@ public record RecipeSummaryResponse(
String status,
int ingredientCount,
int stepCount,
LocalDateTime createdAt,
LocalDateTime updatedAt
OffsetDateTime createdAt,
OffsetDateTime updatedAt
) {
public static RecipeSummaryResponse from(Recipe recipe) {
return new RecipeSummaryResponse(

View file

@ -5,7 +5,7 @@ import jakarta.persistence.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.HashSet;
import java.util.Set;
@ -48,10 +48,10 @@ public class UserEntity {
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
private OffsetDateTime createdAt;
@Column(name = "last_login")
private LocalDateTime lastLogin;
private OffsetDateTime lastLogin;
// JPA requires no-arg constructor
protected UserEntity() {
@ -65,8 +65,8 @@ public class UserEntity {
Set<RoleEntity> roles,
String branchId,
UserStatus status,
LocalDateTime createdAt,
LocalDateTime lastLogin
OffsetDateTime createdAt,
OffsetDateTime lastLogin
) {
this.id = id;
this.username = username;
@ -136,19 +136,19 @@ public class UserEntity {
this.status = status;
}
public LocalDateTime getCreatedAt() {
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getLastLogin() {
public OffsetDateTime getLastLogin() {
return lastLogin;
}
public void setLastLogin(LocalDateTime lastLogin) {
public void setLastLogin(OffsetDateTime 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 java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
/**
@ -23,7 +24,7 @@ public record ErrorResponse(
int status,
@Schema(description = "Timestamp when error occurred")
LocalDateTime timestamp,
OffsetDateTime timestamp,
@Schema(description = "Request path where error occurred", example = "/api/users/user-123")
String path,
@ -44,7 +45,7 @@ public record ErrorResponse(
code,
message,
status,
LocalDateTime.now(),
OffsetDateTime.now(ZoneOffset.UTC),
path,
null
);
@ -63,7 +64,7 @@ public record ErrorResponse(
"VALIDATION_ERROR",
message,
status,
LocalDateTime.now(),
OffsetDateTime.now(ZoneOffset.UTC),
path,
validationErrors
);

View file

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

View file

@ -119,7 +119,7 @@ class AddStockBatchTest {
List.of(StockBatch.reconstitute(
StockBatchId.generate(),
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),
StockBatchStatus.AVAILABLE,
Instant.now()

View file

@ -43,7 +43,7 @@ class RemoveStockBatchTest {
var batch = StockBatch.reconstitute(
batchId,
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),
StockBatchStatus.AVAILABLE,
Instant.now()

View file

@ -15,7 +15,8 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Optional;
@ -61,7 +62,7 @@ class ActivateRecipeTest {
null, new YieldPercentage(85), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
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 java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Optional;
@ -45,7 +46,7 @@ class ArchiveRecipeTest {
null, new YieldPercentage(85), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
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,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
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(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 java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Optional;
@ -43,7 +44,7 @@ class GetRecipeTest {
"Beschreibung", new YieldPercentage(85), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
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 java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@ -42,7 +43,7 @@ class ListRecipesTest {
null, new YieldPercentage(85), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
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.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Optional;
@ -57,7 +58,7 @@ class PlanBatchTest {
null, new YieldPercentage(85), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
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,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
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 java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Optional;
@ -48,7 +49,7 @@ class RecipeCycleCheckerTest {
null, new YieldPercentage(100), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
RecipeStatus.DRAFT, ingredients, List.of(),
LocalDateTime.now(), LocalDateTime.now());
OffsetDateTime.now(ZoneOffset.UTC), OffsetDateTime.now(ZoneOffset.UTC));
}
private Recipe recipeWithoutSubRecipes(String id) {
@ -145,7 +146,7 @@ class RecipeCycleCheckerTest {
null, new YieldPercentage(100), 14,
Quantity.of(new BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
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("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.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashSet;
import java.util.Optional;
@ -42,7 +43,7 @@ class AssignRoleTest {
testUser = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com",
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");
}

View file

@ -13,7 +13,8 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
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.Optional;
@ -45,10 +46,10 @@ class AuthenticateUserTest {
testUser = User.reconstitute(
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
@ -84,7 +85,7 @@ class AuthenticateUserTest {
void should_FailWithLockedUser_When_UserStatusIsLocked() {
User lockedUser = User.reconstitute(
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)));
@ -100,7 +101,7 @@ class AuthenticateUserTest {
void should_FailWithInactiveUser_When_UserStatusIsInactive() {
User inactiveUser = User.reconstitute(
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)));
@ -143,7 +144,7 @@ class AuthenticateUserTest {
void should_NotCreateSession_When_UserLocked() {
User lockedUser = User.reconstitute(
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)));

View file

@ -12,7 +12,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
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.Optional;
@ -44,7 +45,7 @@ class ChangePasswordTest {
testUser = User.reconstitute(
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!");

View file

@ -12,7 +12,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
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.Optional;
@ -37,7 +38,7 @@ class GetUserTest {
testUser = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com",
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.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashSet;
import java.util.List;
@ -39,12 +40,12 @@ class ListUsersTest {
user1 = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com",
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(
UserId.of("user-2"), "jane.doe", "jane@example.com",
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.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashSet;
import java.util.Optional;
@ -40,7 +41,7 @@ class LockUserTest {
activeUser = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com",
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(
UserId.of("user-2"), "jane.doe", "jane@example.com",
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(userRepository.findById(UserId.of("user-2"))).thenReturn(Result.success(Optional.of(lockedUser)));
@ -118,7 +119,7 @@ class LockUserTest {
User inactiveUser = User.reconstitute(
UserId.of("user-3"), "bob", "bob@example.com",
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(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.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
@ -44,7 +45,7 @@ class RemoveRoleTest {
userWithRole = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com",
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.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashSet;
import java.util.Optional;
@ -40,7 +41,7 @@ class UnlockUserTest {
lockedUser = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com",
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(
UserId.of("user-2"), "jane.doe", "jane@example.com",
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(userRepository.findById(UserId.of("user-2"))).thenReturn(Result.success(Optional.of(activeUser)));
@ -118,7 +119,7 @@ class UnlockUserTest {
User inactiveUser = User.reconstitute(
UserId.of("user-3"), "bob", "bob@example.com",
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(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.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashSet;
import java.util.Optional;
@ -40,7 +41,7 @@ class UpdateUserTest {
testUser = User.reconstitute(
UserId.of("user-1"), "john.doe", "john@example.com",
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 articleId = ArticleId.of("article-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 minimumShelfLife = new MinimumShelfLife(30);
@ -606,7 +606,7 @@ class StockTest {
var batch = StockBatch.reconstitute(
StockBatchId.generate(),
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),
status,
Instant.now()

View file

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

View file

@ -239,7 +239,7 @@ class RecipeTest {
null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
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));
@ -322,7 +322,7 @@ class RecipeTest {
null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
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());
@ -376,7 +376,7 @@ class RecipeTest {
null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
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));
@ -445,7 +445,7 @@ class RecipeTest {
null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
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);
@ -507,7 +507,7 @@ class RecipeTest {
null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
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();
@ -528,7 +528,7 @@ class RecipeTest {
null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
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();
@ -554,7 +554,7 @@ class RecipeTest {
null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
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();
@ -588,7 +588,7 @@ class RecipeTest {
null, new YieldPercentage(85), 14,
Quantity.of(new java.math.BigDecimal("100"), UnitOfMeasure.KILOGRAM).unsafeGetValue(),
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();

View file

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

View file

@ -18,7 +18,8 @@ import org.springframework.transaction.annotation.Transactional;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.Set;
import java.util.UUID;
@ -102,7 +103,7 @@ public abstract class AbstractIntegrationTest {
UserEntity user = new UserEntity(
UUID.randomUUID().toString(), username, email,
BCRYPT_PASS123, roles,
branchId, UserStatus.ACTIVE, LocalDateTime.now(), null);
branchId, UserStatus.ACTIVE, OffsetDateTime.now(ZoneOffset.UTC), null);
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.Test;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.HashSet;
import java.util.Set;
@ -29,11 +30,11 @@ class UserMapperTest {
private User domainUser;
private UserEntity jpaEntity;
private LocalDateTime createdAt;
private OffsetDateTime createdAt;
@BeforeEach
void setUp() {
createdAt = LocalDateTime.now();
createdAt = OffsetDateTime.now(ZoneOffset.UTC);
// Create JPA entity first
jpaEntity = new UserEntity(
@ -112,7 +113,7 @@ class UserMapperTest {
@DisplayName("should_PreserveAllUserFields_When_MappingToEntity")
void should_PreserveAllUserFields_When_MappingToEntity() {
// Arrange
LocalDateTime lastLogin = LocalDateTime.now();
OffsetDateTime lastLogin = OffsetDateTime.now(ZoneOffset.UTC);
UserEntity sourceEntity = new UserEntity(
"user-456",
"jane.smith",
@ -144,7 +145,7 @@ class UserMapperTest {
@DisplayName("should_PreserveAllEntityFields_When_MappingToDomain")
void should_PreserveAllEntityFields_When_MappingToDomain() {
// Arrange
LocalDateTime lastLogin = LocalDateTime.now();
OffsetDateTime lastLogin = OffsetDateTime.now(ZoneOffset.UTC);
UserEntity entityWithLastLogin = new UserEntity(
"user-789",
"bob.jones",

View file

@ -18,7 +18,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
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.Set;
import java.util.UUID;
@ -327,7 +328,7 @@ class SecurityIntegrationTest extends AbstractIntegrationTest {
.orElseThrow();
assertThat(auditLog.getTimestamp()).isNotNull();
assertThat(auditLog.getTimestamp()).isAfter(LocalDateTime.now().minusMinutes(1));
assertThat(auditLog.getTimestamp()).isAfter(OffsetDateTime.now(ZoneOffset.UTC).minusMinutes(1));
}
@Test