mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 13:59:36 +01:00
feat(production): Quantity VO mit Catch-Weight, YieldPercentage und BatchNumber
Shared Value Objects für den Production BC implementiert (#25): - Quantity mit Dual-Quantity/Catch-Weight Support und Arithmetik - UnitOfMeasure Enum (kg, g, L, mL, pc, m) - YieldPercentage (1-200) mit calculateRequiredInput() - BatchNumber mit Format P-YYYY-MM-DD-XXX - QuantityError sealed interface für funktionales Error Handling - 60 Unit Tests für alle VOs
This commit is contained in:
parent
c474388f32
commit
a1161cfbad
9 changed files with 968 additions and 0 deletions
|
|
@ -0,0 +1,64 @@
|
||||||
|
package de.effigenix.domain.production;
|
||||||
|
|
||||||
|
import de.effigenix.shared.common.Result;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value Object representing a production batch number.
|
||||||
|
*
|
||||||
|
* <p>Format: {@code P-YYYY-MM-DD-XXX} where XXX is a 3-digit sequence number (001–999).
|
||||||
|
*
|
||||||
|
* <p>Invariant: must match the pattern P-YYYY-MM-DD-XXX
|
||||||
|
*/
|
||||||
|
public record BatchNumber(String value) {
|
||||||
|
|
||||||
|
private static final Pattern FORMAT = Pattern.compile("^P-\\d{4}-\\d{2}-\\d{2}-\\d{3}$");
|
||||||
|
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
|
||||||
|
public BatchNumber {
|
||||||
|
if (value == null || !FORMAT.matcher(value).matches()) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Batch number must match format P-YYYY-MM-DD-XXX, was: " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a BatchNumber, returning a Result.
|
||||||
|
*/
|
||||||
|
public static Result<QuantityError, BatchNumber> of(String value) {
|
||||||
|
if (value == null || !FORMAT.matcher(value).matches()) {
|
||||||
|
return Result.failure(new QuantityError.InvalidBatchNumber(value));
|
||||||
|
}
|
||||||
|
return Result.success(new BatchNumber(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new BatchNumber for the given date and sequence number.
|
||||||
|
*/
|
||||||
|
public static BatchNumber generate(LocalDate date, int sequenceNumber) {
|
||||||
|
if (sequenceNumber < 1 || sequenceNumber > 999) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Sequence number must be between 1 and 999, was: " + sequenceNumber);
|
||||||
|
}
|
||||||
|
String formatted = String.format("P-%s-%03d", date.format(DATE_FORMAT), sequenceNumber);
|
||||||
|
return new BatchNumber(formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the date portion from the batch number.
|
||||||
|
*/
|
||||||
|
public LocalDate date() {
|
||||||
|
String datePart = value.substring(2, 12);
|
||||||
|
return LocalDate.parse(datePart, DATE_FORMAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the sequence number from the batch number.
|
||||||
|
*/
|
||||||
|
public int sequenceNumber() {
|
||||||
|
return Integer.parseInt(value.substring(13));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,186 @@
|
||||||
|
package de.effigenix.domain.production;
|
||||||
|
|
||||||
|
import de.effigenix.shared.common.Result;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value Object representing a production quantity with optional Catch-Weight (dual quantity).
|
||||||
|
*
|
||||||
|
* <p>Primary quantity is always required (amount + unit of measure).
|
||||||
|
* Secondary quantity (catch-weight) is optional and used when goods are counted
|
||||||
|
* in one unit but weighed in another (e.g., 10 pieces = 23 kg).
|
||||||
|
*
|
||||||
|
* <p>Invariants:
|
||||||
|
* <ul>
|
||||||
|
* <li>Primary amount must be positive</li>
|
||||||
|
* <li>Secondary amount, if present, must be positive</li>
|
||||||
|
* <li>Secondary UoM is required when secondary amount is provided (and vice versa)</li>
|
||||||
|
* <li>Arithmetic operations require matching primary UoM</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public final class Quantity {
|
||||||
|
|
||||||
|
private static final int SCALE = 6;
|
||||||
|
|
||||||
|
private final BigDecimal amount;
|
||||||
|
private final UnitOfMeasure uom;
|
||||||
|
private final BigDecimal secondaryAmount;
|
||||||
|
private final UnitOfMeasure secondaryUom;
|
||||||
|
|
||||||
|
private Quantity(BigDecimal amount, UnitOfMeasure uom,
|
||||||
|
BigDecimal secondaryAmount, UnitOfMeasure secondaryUom) {
|
||||||
|
this.amount = amount;
|
||||||
|
this.uom = uom;
|
||||||
|
this.secondaryAmount = secondaryAmount;
|
||||||
|
this.secondaryUom = secondaryUom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a simple quantity with one unit of measure.
|
||||||
|
*/
|
||||||
|
public static Result<QuantityError, Quantity> of(BigDecimal amount, UnitOfMeasure uom) {
|
||||||
|
Objects.requireNonNull(uom, "Unit of measure must not be null");
|
||||||
|
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
return Result.failure(new QuantityError.AmountMustBePositive(
|
||||||
|
amount == null ? "null" : amount.toPlainString()));
|
||||||
|
}
|
||||||
|
return Result.success(new Quantity(scale(amount), uom, null, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a dual quantity (catch-weight) with primary and secondary units.
|
||||||
|
*/
|
||||||
|
public static Result<QuantityError, Quantity> dual(BigDecimal amount, UnitOfMeasure uom,
|
||||||
|
BigDecimal secondaryAmount, UnitOfMeasure secondaryUom) {
|
||||||
|
Objects.requireNonNull(uom, "Unit of measure must not be null");
|
||||||
|
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
return Result.failure(new QuantityError.AmountMustBePositive(
|
||||||
|
amount == null ? "null" : amount.toPlainString()));
|
||||||
|
}
|
||||||
|
if (secondaryAmount == null || secondaryAmount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
return Result.failure(new QuantityError.SecondaryAmountMustBePositive(
|
||||||
|
secondaryAmount == null ? "null" : secondaryAmount.toPlainString()));
|
||||||
|
}
|
||||||
|
if (secondaryUom == null) {
|
||||||
|
return Result.failure(new QuantityError.SecondaryUomRequired());
|
||||||
|
}
|
||||||
|
return Result.success(new Quantity(scale(amount), uom, scale(secondaryAmount), secondaryUom));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconstitutes a Quantity from persistence. No validation.
|
||||||
|
*/
|
||||||
|
public static Quantity reconstitute(BigDecimal amount, UnitOfMeasure uom,
|
||||||
|
BigDecimal secondaryAmount, UnitOfMeasure secondaryUom) {
|
||||||
|
return new Quantity(amount, uom, secondaryAmount, secondaryUom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds two quantities. Both must have the same primary UoM.
|
||||||
|
* Secondary quantities are added if both are present.
|
||||||
|
*/
|
||||||
|
public Result<QuantityError, Quantity> add(Quantity other) {
|
||||||
|
if (this.uom != other.uom) {
|
||||||
|
return Result.failure(new QuantityError.UnitOfMeasureMismatch(this.uom, other.uom));
|
||||||
|
}
|
||||||
|
BigDecimal newSecondary = addSecondary(this.secondaryAmount, other.secondaryAmount);
|
||||||
|
return Result.success(new Quantity(
|
||||||
|
scale(this.amount.add(other.amount)), this.uom,
|
||||||
|
newSecondary, this.secondaryAmount != null ? this.secondaryUom : other.secondaryUom));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subtracts another quantity. Both must have the same primary UoM.
|
||||||
|
* Result amount must remain positive.
|
||||||
|
*/
|
||||||
|
public Result<QuantityError, Quantity> subtract(Quantity other) {
|
||||||
|
if (this.uom != other.uom) {
|
||||||
|
return Result.failure(new QuantityError.UnitOfMeasureMismatch(this.uom, other.uom));
|
||||||
|
}
|
||||||
|
BigDecimal newAmount = this.amount.subtract(other.amount);
|
||||||
|
if (newAmount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
return Result.failure(new QuantityError.AmountMustBePositive(newAmount.toPlainString()));
|
||||||
|
}
|
||||||
|
BigDecimal newSecondary = subtractSecondary(this.secondaryAmount, other.secondaryAmount);
|
||||||
|
return Result.success(new Quantity(
|
||||||
|
scale(newAmount), this.uom,
|
||||||
|
newSecondary, this.secondaryUom));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiplies this quantity by a factor.
|
||||||
|
*/
|
||||||
|
public Result<QuantityError, Quantity> multiply(BigDecimal factor) {
|
||||||
|
if (factor == null || factor.compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
return Result.failure(new QuantityError.AmountMustBePositive(
|
||||||
|
factor == null ? "null" : factor.toPlainString()));
|
||||||
|
}
|
||||||
|
BigDecimal newAmount = scale(this.amount.multiply(factor));
|
||||||
|
BigDecimal newSecondary = this.secondaryAmount != null
|
||||||
|
? scale(this.secondaryAmount.multiply(factor))
|
||||||
|
: null;
|
||||||
|
return Result.success(new Quantity(newAmount, this.uom, newSecondary, this.secondaryUom));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isZero() {
|
||||||
|
return amount.compareTo(BigDecimal.ZERO) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPositive() {
|
||||||
|
return amount.compareTo(BigDecimal.ZERO) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasDualQuantity() {
|
||||||
|
return secondaryAmount != null && secondaryUom != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal amount() { return amount; }
|
||||||
|
public UnitOfMeasure uom() { return uom; }
|
||||||
|
public BigDecimal secondaryAmount() { return secondaryAmount; }
|
||||||
|
public UnitOfMeasure secondaryUom() { return secondaryUom; }
|
||||||
|
|
||||||
|
private static BigDecimal scale(BigDecimal value) {
|
||||||
|
return value.setScale(SCALE, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigDecimal addSecondary(BigDecimal a, BigDecimal b) {
|
||||||
|
if (a != null && b != null) return scale(a.add(b));
|
||||||
|
return a != null ? a : b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigDecimal subtractSecondary(BigDecimal a, BigDecimal b) {
|
||||||
|
if (a != null && b != null) return scale(a.subtract(b));
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (!(o instanceof Quantity that)) return false;
|
||||||
|
return amount.compareTo(that.amount) == 0
|
||||||
|
&& uom == that.uom
|
||||||
|
&& (Objects.equals(secondaryAmount, that.secondaryAmount)
|
||||||
|
|| (secondaryAmount != null && that.secondaryAmount != null
|
||||||
|
&& secondaryAmount.compareTo(that.secondaryAmount) == 0))
|
||||||
|
&& secondaryUom == that.secondaryUom;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(amount.stripTrailingZeros(), uom,
|
||||||
|
secondaryAmount != null ? secondaryAmount.stripTrailingZeros() : null,
|
||||||
|
secondaryUom);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
if (hasDualQuantity()) {
|
||||||
|
return amount.toPlainString() + " " + uom.symbol()
|
||||||
|
+ " (" + secondaryAmount.toPlainString() + " " + secondaryUom.symbol() + ")";
|
||||||
|
}
|
||||||
|
return amount.toPlainString() + " " + uom.symbol();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package de.effigenix.domain.production;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain errors for Quantity-related Value Objects.
|
||||||
|
*/
|
||||||
|
public sealed interface QuantityError {
|
||||||
|
|
||||||
|
String code();
|
||||||
|
String message();
|
||||||
|
|
||||||
|
record AmountMustBePositive(String amount) implements QuantityError {
|
||||||
|
@Override public String code() { return "QUANTITY_AMOUNT_NOT_POSITIVE"; }
|
||||||
|
@Override public String message() { return "Quantity amount must be positive, was: " + amount; }
|
||||||
|
}
|
||||||
|
|
||||||
|
record UnitOfMeasureMismatch(UnitOfMeasure expected, UnitOfMeasure actual) implements QuantityError {
|
||||||
|
@Override public String code() { return "QUANTITY_UOM_MISMATCH"; }
|
||||||
|
@Override public String message() { return "Unit of measure mismatch: expected " + expected.symbol() + ", got " + actual.symbol(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
record SecondaryAmountMustBePositive(String amount) implements QuantityError {
|
||||||
|
@Override public String code() { return "QUANTITY_SECONDARY_AMOUNT_NOT_POSITIVE"; }
|
||||||
|
@Override public String message() { return "Secondary quantity amount must be positive, was: " + amount; }
|
||||||
|
}
|
||||||
|
|
||||||
|
record SecondaryUomRequired() implements QuantityError {
|
||||||
|
@Override public String code() { return "QUANTITY_SECONDARY_UOM_REQUIRED"; }
|
||||||
|
@Override public String message() { return "Secondary unit of measure is required when secondary amount is provided"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
record InvalidYieldPercentage(int value) implements QuantityError {
|
||||||
|
@Override public String code() { return "INVALID_YIELD_PERCENTAGE"; }
|
||||||
|
@Override public String message() { return "Yield percentage must be between 1 and 200, was: " + value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
record InvalidBatchNumber(String value) implements QuantityError {
|
||||||
|
@Override public String code() { return "INVALID_BATCH_NUMBER"; }
|
||||||
|
@Override public String message() { return "Batch number must match format P-YYYY-MM-DD-XXX, was: " + value; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package de.effigenix.domain.production;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit of Measure for production quantities.
|
||||||
|
* Supports mass, volume, length, and piece-based units.
|
||||||
|
*/
|
||||||
|
public enum UnitOfMeasure {
|
||||||
|
KILOGRAM("kg"),
|
||||||
|
GRAM("g"),
|
||||||
|
LITER("L"),
|
||||||
|
MILLILITER("mL"),
|
||||||
|
PIECE("pc"),
|
||||||
|
METER("m");
|
||||||
|
|
||||||
|
private final String symbol;
|
||||||
|
|
||||||
|
UnitOfMeasure(String symbol) {
|
||||||
|
this.symbol = symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String symbol() {
|
||||||
|
return symbol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package de.effigenix.domain.production;
|
||||||
|
|
||||||
|
import de.effigenix.shared.common.Result;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value Object representing production yield as a percentage (1–200).
|
||||||
|
*
|
||||||
|
* <p>Used to calculate required input quantities based on expected output.
|
||||||
|
* A yield of 80% means that 80% of input becomes output, so to produce
|
||||||
|
* 100 kg output you need 125 kg input (100 / 0.80).
|
||||||
|
*
|
||||||
|
* <p>Invariant: value must be between 1 and 200 (inclusive)
|
||||||
|
*/
|
||||||
|
public record YieldPercentage(int value) {
|
||||||
|
|
||||||
|
public YieldPercentage {
|
||||||
|
if (value < 1 || value > 200) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Yield percentage must be between 1 and 200, was: " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a YieldPercentage, returning a Result.
|
||||||
|
*/
|
||||||
|
public static Result<QuantityError, YieldPercentage> of(int value) {
|
||||||
|
if (value < 1 || value > 200) {
|
||||||
|
return Result.failure(new QuantityError.InvalidYieldPercentage(value));
|
||||||
|
}
|
||||||
|
return Result.success(new YieldPercentage(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the required input quantity to achieve the desired output quantity
|
||||||
|
* at this yield percentage.
|
||||||
|
*
|
||||||
|
* <p>Formula: requiredInput = desiredOutput / (yieldPercentage / 100)
|
||||||
|
* <p>Example: YieldPercentage(80).calculateRequiredInput(100 kg) = 125 kg
|
||||||
|
*/
|
||||||
|
public Quantity calculateRequiredInput(Quantity desiredOutput) {
|
||||||
|
BigDecimal factor = new BigDecimal(100)
|
||||||
|
.divide(new BigDecimal(value), 10, RoundingMode.HALF_UP);
|
||||||
|
BigDecimal requiredAmount = desiredOutput.amount()
|
||||||
|
.multiply(factor)
|
||||||
|
.setScale(6, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
|
BigDecimal requiredSecondary = desiredOutput.secondaryAmount() != null
|
||||||
|
? desiredOutput.secondaryAmount().multiply(factor).setScale(6, RoundingMode.HALF_UP)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return Quantity.reconstitute(requiredAmount, desiredOutput.uom(),
|
||||||
|
requiredSecondary, desiredOutput.secondaryUom());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the yield as a decimal factor (e.g., 80 → 0.80).
|
||||||
|
*/
|
||||||
|
public BigDecimal asFactor() {
|
||||||
|
return new BigDecimal(value)
|
||||||
|
.divide(new BigDecimal(100), 10, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
package de.effigenix.domain.production;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
@DisplayName("BatchNumber Value Object")
|
||||||
|
class BatchNumberTest {
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("of() factory")
|
||||||
|
class OfFactory {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should create BatchNumber with valid format")
|
||||||
|
void should_Create_When_ValidFormat() {
|
||||||
|
var result = BatchNumber.of("P-2026-02-19-001");
|
||||||
|
|
||||||
|
assertThat(result.isSuccess()).isTrue();
|
||||||
|
assertThat(result.unsafeGetValue().value()).isEqualTo("P-2026-02-19-001");
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = {
|
||||||
|
"P-2026-01-15-999",
|
||||||
|
"P-2025-12-31-042",
|
||||||
|
"P-2024-06-01-001"
|
||||||
|
})
|
||||||
|
@DisplayName("should accept various valid batch numbers")
|
||||||
|
void should_Accept_When_ValidFormats(String batchNumber) {
|
||||||
|
var result = BatchNumber.of(batchNumber);
|
||||||
|
assertThat(result.isSuccess()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = {
|
||||||
|
"2026-02-19-001",
|
||||||
|
"P-2026-02-19-01",
|
||||||
|
"P-2026-02-19-1000",
|
||||||
|
"X-2026-02-19-001",
|
||||||
|
"P-26-02-19-001",
|
||||||
|
"P2026-02-19-001",
|
||||||
|
"",
|
||||||
|
"random-string"
|
||||||
|
})
|
||||||
|
@DisplayName("should fail for invalid formats")
|
||||||
|
void should_Fail_When_InvalidFormat(String batchNumber) {
|
||||||
|
var result = BatchNumber.of(batchNumber);
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.InvalidBatchNumber.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should fail for null value")
|
||||||
|
void should_Fail_When_Null() {
|
||||||
|
var result = BatchNumber.of(null);
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("generate()")
|
||||||
|
class Generate {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should generate batch number for date and sequence")
|
||||||
|
void should_Generate_When_ValidDateAndSequence() {
|
||||||
|
var bn = BatchNumber.generate(LocalDate.of(2026, 2, 19), 1);
|
||||||
|
|
||||||
|
assertThat(bn.value()).isEqualTo("P-2026-02-19-001");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should generate with 3-digit sequence")
|
||||||
|
void should_PadSequence_When_SmallNumber() {
|
||||||
|
var bn = BatchNumber.generate(LocalDate.of(2026, 2, 19), 42);
|
||||||
|
|
||||||
|
assertThat(bn.value()).isEqualTo("P-2026-02-19-042");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should throw when sequence out of range")
|
||||||
|
void should_Throw_When_SequenceOutOfRange() {
|
||||||
|
assertThatThrownBy(() -> BatchNumber.generate(LocalDate.of(2026, 1, 1), 0))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> BatchNumber.generate(LocalDate.of(2026, 1, 1), 1000))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Extraction methods")
|
||||||
|
class Extraction {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should extract date from batch number")
|
||||||
|
void should_ExtractDate() {
|
||||||
|
var bn = new BatchNumber("P-2026-02-19-001");
|
||||||
|
assertThat(bn.date()).isEqualTo(LocalDate.of(2026, 2, 19));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should extract sequence number from batch number")
|
||||||
|
void should_ExtractSequence() {
|
||||||
|
var bn = new BatchNumber("P-2026-02-19-042");
|
||||||
|
assertThat(bn.sequenceNumber()).isEqualTo(42);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Equality")
|
||||||
|
class Equality {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should be equal when same value")
|
||||||
|
void should_BeEqual_When_SameValue() {
|
||||||
|
var bn1 = new BatchNumber("P-2026-02-19-001");
|
||||||
|
var bn2 = new BatchNumber("P-2026-02-19-001");
|
||||||
|
|
||||||
|
assertThat(bn1).isEqualTo(bn2);
|
||||||
|
assertThat(bn1.hashCode()).isEqualTo(bn2.hashCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should not be equal when different value")
|
||||||
|
void should_NotBeEqual_When_DifferentValue() {
|
||||||
|
var bn1 = new BatchNumber("P-2026-02-19-001");
|
||||||
|
var bn2 = new BatchNumber("P-2026-02-19-002");
|
||||||
|
|
||||||
|
assertThat(bn1).isNotEqualTo(bn2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,283 @@
|
||||||
|
package de.effigenix.domain.production;
|
||||||
|
|
||||||
|
import de.effigenix.shared.common.Result;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
import static de.effigenix.domain.production.UnitOfMeasure.*;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@DisplayName("Quantity Value Object")
|
||||||
|
class QuantityTest {
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("of() factory")
|
||||||
|
class OfFactory {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should create Quantity with valid amount and UoM")
|
||||||
|
void should_CreateQuantity_When_ValidAmountAndUom() {
|
||||||
|
var result = Quantity.of(new BigDecimal("10"), KILOGRAM);
|
||||||
|
|
||||||
|
assertThat(result.isSuccess()).isTrue();
|
||||||
|
var qty = result.unsafeGetValue();
|
||||||
|
assertThat(qty.amount()).isEqualByComparingTo(new BigDecimal("10"));
|
||||||
|
assertThat(qty.uom()).isEqualTo(KILOGRAM);
|
||||||
|
assertThat(qty.hasDualQuantity()).isFalse();
|
||||||
|
assertThat(qty.secondaryAmount()).isNull();
|
||||||
|
assertThat(qty.secondaryUom()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should fail when amount is zero")
|
||||||
|
void should_Fail_When_AmountIsZero() {
|
||||||
|
var result = Quantity.of(BigDecimal.ZERO, KILOGRAM);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.AmountMustBePositive.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should fail when amount is negative")
|
||||||
|
void should_Fail_When_AmountIsNegative() {
|
||||||
|
var result = Quantity.of(new BigDecimal("-5"), KILOGRAM);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.AmountMustBePositive.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should fail when amount is null")
|
||||||
|
void should_Fail_When_AmountIsNull() {
|
||||||
|
var result = Quantity.of(null, KILOGRAM);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.AmountMustBePositive.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("dual() factory")
|
||||||
|
class DualFactory {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should create dual Quantity with primary and secondary")
|
||||||
|
void should_CreateDualQuantity_When_ValidInputs() {
|
||||||
|
var result = Quantity.dual(
|
||||||
|
new BigDecimal("10"), PIECE,
|
||||||
|
new BigDecimal("23"), KILOGRAM);
|
||||||
|
|
||||||
|
assertThat(result.isSuccess()).isTrue();
|
||||||
|
var qty = result.unsafeGetValue();
|
||||||
|
assertThat(qty.hasDualQuantity()).isTrue();
|
||||||
|
assertThat(qty.amount()).isEqualByComparingTo(new BigDecimal("10"));
|
||||||
|
assertThat(qty.uom()).isEqualTo(PIECE);
|
||||||
|
assertThat(qty.secondaryAmount()).isEqualByComparingTo(new BigDecimal("23"));
|
||||||
|
assertThat(qty.secondaryUom()).isEqualTo(KILOGRAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should fail when secondary amount is zero")
|
||||||
|
void should_Fail_When_SecondaryAmountIsZero() {
|
||||||
|
var result = Quantity.dual(
|
||||||
|
new BigDecimal("10"), PIECE,
|
||||||
|
BigDecimal.ZERO, KILOGRAM);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.SecondaryAmountMustBePositive.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should fail when secondary UoM is null")
|
||||||
|
void should_Fail_When_SecondaryUomIsNull() {
|
||||||
|
var result = Quantity.dual(
|
||||||
|
new BigDecimal("10"), PIECE,
|
||||||
|
new BigDecimal("23"), null);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.SecondaryUomRequired.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should fail when primary amount is invalid")
|
||||||
|
void should_Fail_When_PrimaryAmountInvalid() {
|
||||||
|
var result = Quantity.dual(
|
||||||
|
BigDecimal.ZERO, PIECE,
|
||||||
|
new BigDecimal("23"), KILOGRAM);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.AmountMustBePositive.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Arithmetic operations")
|
||||||
|
class Arithmetic {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should add quantities with same UoM")
|
||||||
|
void should_Add_When_SameUom() {
|
||||||
|
var q1 = Quantity.of(new BigDecimal("10"), KILOGRAM).unsafeGetValue();
|
||||||
|
var q2 = Quantity.of(new BigDecimal("5"), KILOGRAM).unsafeGetValue();
|
||||||
|
|
||||||
|
var result = q1.add(q2);
|
||||||
|
|
||||||
|
assertThat(result.isSuccess()).isTrue();
|
||||||
|
assertThat(result.unsafeGetValue().amount()).isEqualByComparingTo(new BigDecimal("15"));
|
||||||
|
assertThat(result.unsafeGetValue().uom()).isEqualTo(KILOGRAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should fail to add quantities with different UoM")
|
||||||
|
void should_FailAdd_When_DifferentUom() {
|
||||||
|
var q1 = Quantity.of(new BigDecimal("10"), KILOGRAM).unsafeGetValue();
|
||||||
|
var q2 = Quantity.of(new BigDecimal("5"), LITER).unsafeGetValue();
|
||||||
|
|
||||||
|
var result = q1.add(q2);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.UnitOfMeasureMismatch.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should subtract quantities with same UoM")
|
||||||
|
void should_Subtract_When_SameUom() {
|
||||||
|
var q1 = Quantity.of(new BigDecimal("10"), KILOGRAM).unsafeGetValue();
|
||||||
|
var q2 = Quantity.of(new BigDecimal("3"), KILOGRAM).unsafeGetValue();
|
||||||
|
|
||||||
|
var result = q1.subtract(q2);
|
||||||
|
|
||||||
|
assertThat(result.isSuccess()).isTrue();
|
||||||
|
assertThat(result.unsafeGetValue().amount()).isEqualByComparingTo(new BigDecimal("7"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should fail to subtract when result would not be positive")
|
||||||
|
void should_FailSubtract_When_ResultNotPositive() {
|
||||||
|
var q1 = Quantity.of(new BigDecimal("5"), KILOGRAM).unsafeGetValue();
|
||||||
|
var q2 = Quantity.of(new BigDecimal("5"), KILOGRAM).unsafeGetValue();
|
||||||
|
|
||||||
|
var result = q1.subtract(q2);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.AmountMustBePositive.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should fail to subtract with different UoM")
|
||||||
|
void should_FailSubtract_When_DifferentUom() {
|
||||||
|
var q1 = Quantity.of(new BigDecimal("10"), KILOGRAM).unsafeGetValue();
|
||||||
|
var q2 = Quantity.of(new BigDecimal("3"), GRAM).unsafeGetValue();
|
||||||
|
|
||||||
|
var result = q1.subtract(q2);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.UnitOfMeasureMismatch.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should multiply quantity by factor")
|
||||||
|
void should_Multiply_When_ValidFactor() {
|
||||||
|
var q = Quantity.of(new BigDecimal("10"), KILOGRAM).unsafeGetValue();
|
||||||
|
|
||||||
|
var result = q.multiply(new BigDecimal("2.5"));
|
||||||
|
|
||||||
|
assertThat(result.isSuccess()).isTrue();
|
||||||
|
assertThat(result.unsafeGetValue().amount()).isEqualByComparingTo(new BigDecimal("25"));
|
||||||
|
assertThat(result.unsafeGetValue().uom()).isEqualTo(KILOGRAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should fail to multiply by zero")
|
||||||
|
void should_FailMultiply_When_FactorIsZero() {
|
||||||
|
var q = Quantity.of(new BigDecimal("10"), KILOGRAM).unsafeGetValue();
|
||||||
|
|
||||||
|
var result = q.multiply(BigDecimal.ZERO);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should add dual quantities preserving secondary")
|
||||||
|
void should_AddDual_When_BothHaveSecondary() {
|
||||||
|
var q1 = Quantity.dual(new BigDecimal("10"), PIECE, new BigDecimal("20"), KILOGRAM).unsafeGetValue();
|
||||||
|
var q2 = Quantity.dual(new BigDecimal("5"), PIECE, new BigDecimal("10"), KILOGRAM).unsafeGetValue();
|
||||||
|
|
||||||
|
var result = q1.add(q2);
|
||||||
|
|
||||||
|
assertThat(result.isSuccess()).isTrue();
|
||||||
|
var sum = result.unsafeGetValue();
|
||||||
|
assertThat(sum.amount()).isEqualByComparingTo(new BigDecimal("15"));
|
||||||
|
assertThat(sum.secondaryAmount()).isEqualByComparingTo(new BigDecimal("30"));
|
||||||
|
assertThat(sum.secondaryUom()).isEqualTo(KILOGRAM);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Query methods")
|
||||||
|
class QueryMethods {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("isPositive returns true for positive quantity")
|
||||||
|
void should_BePositive_When_AmountIsPositive() {
|
||||||
|
var q = Quantity.of(new BigDecimal("10"), KILOGRAM).unsafeGetValue();
|
||||||
|
assertThat(q.isPositive()).isTrue();
|
||||||
|
assertThat(q.isZero()).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Equality")
|
||||||
|
class Equality {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should be equal when same amount and UoM")
|
||||||
|
void should_BeEqual_When_SameValues() {
|
||||||
|
var q1 = Quantity.of(new BigDecimal("10"), KILOGRAM).unsafeGetValue();
|
||||||
|
var q2 = Quantity.of(new BigDecimal("10.000000"), KILOGRAM).unsafeGetValue();
|
||||||
|
|
||||||
|
assertThat(q1).isEqualTo(q2);
|
||||||
|
assertThat(q1.hashCode()).isEqualTo(q2.hashCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should not be equal when different amount")
|
||||||
|
void should_NotBeEqual_When_DifferentAmount() {
|
||||||
|
var q1 = Quantity.of(new BigDecimal("10"), KILOGRAM).unsafeGetValue();
|
||||||
|
var q2 = Quantity.of(new BigDecimal("20"), KILOGRAM).unsafeGetValue();
|
||||||
|
|
||||||
|
assertThat(q1).isNotEqualTo(q2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should not be equal when different UoM")
|
||||||
|
void should_NotBeEqual_When_DifferentUom() {
|
||||||
|
var q1 = Quantity.of(new BigDecimal("10"), KILOGRAM).unsafeGetValue();
|
||||||
|
var q2 = Quantity.of(new BigDecimal("10"), GRAM).unsafeGetValue();
|
||||||
|
|
||||||
|
assertThat(q1).isNotEqualTo(q2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("toString()")
|
||||||
|
class ToStringTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should format simple quantity")
|
||||||
|
void should_FormatSimple() {
|
||||||
|
var q = Quantity.of(new BigDecimal("10.5"), KILOGRAM).unsafeGetValue();
|
||||||
|
assertThat(q.toString()).contains("10.5").contains("kg");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should format dual quantity")
|
||||||
|
void should_FormatDual() {
|
||||||
|
var q = Quantity.dual(new BigDecimal("10"), PIECE, new BigDecimal("23"), KILOGRAM).unsafeGetValue();
|
||||||
|
assertThat(q.toString()).contains("10").contains("pc").contains("23").contains("kg");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package de.effigenix.domain.production;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@DisplayName("UnitOfMeasure Enum")
|
||||||
|
class UnitOfMeasureTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should have 6 values")
|
||||||
|
void should_HaveSixValues() {
|
||||||
|
assertThat(UnitOfMeasure.values()).hasSize(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should have correct symbols")
|
||||||
|
void should_HaveCorrectSymbols() {
|
||||||
|
assertThat(UnitOfMeasure.KILOGRAM.symbol()).isEqualTo("kg");
|
||||||
|
assertThat(UnitOfMeasure.GRAM.symbol()).isEqualTo("g");
|
||||||
|
assertThat(UnitOfMeasure.LITER.symbol()).isEqualTo("L");
|
||||||
|
assertThat(UnitOfMeasure.MILLILITER.symbol()).isEqualTo("mL");
|
||||||
|
assertThat(UnitOfMeasure.PIECE.symbol()).isEqualTo("pc");
|
||||||
|
assertThat(UnitOfMeasure.METER.symbol()).isEqualTo("m");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
package de.effigenix.domain.production;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
import static de.effigenix.domain.production.UnitOfMeasure.*;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
@DisplayName("YieldPercentage Value Object")
|
||||||
|
class YieldPercentageTest {
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Creation")
|
||||||
|
class Creation {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should create YieldPercentage with valid value")
|
||||||
|
void should_Create_When_ValidValue() {
|
||||||
|
var result = YieldPercentage.of(80);
|
||||||
|
|
||||||
|
assertThat(result.isSuccess()).isTrue();
|
||||||
|
assertThat(result.unsafeGetValue().value()).isEqualTo(80);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(ints = {1, 100, 200})
|
||||||
|
@DisplayName("should accept boundary values")
|
||||||
|
void should_Accept_When_BoundaryValues(int value) {
|
||||||
|
var result = YieldPercentage.of(value);
|
||||||
|
assertThat(result.isSuccess()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should fail when value is 0")
|
||||||
|
void should_Fail_When_ValueIsZero() {
|
||||||
|
var result = YieldPercentage.of(0);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.InvalidYieldPercentage.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should fail when value is 201")
|
||||||
|
void should_Fail_When_ValueIs201() {
|
||||||
|
var result = YieldPercentage.of(201);
|
||||||
|
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
assertThat(result.unsafeGetError()).isInstanceOf(QuantityError.InvalidYieldPercentage.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(ints = {-1, 0, 201, 300})
|
||||||
|
@DisplayName("should fail for out-of-range values")
|
||||||
|
void should_Fail_When_OutOfRange(int value) {
|
||||||
|
var result = YieldPercentage.of(value);
|
||||||
|
assertThat(result.isFailure()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should throw from constructor for invalid value")
|
||||||
|
void should_ThrowFromConstructor_When_InvalidValue() {
|
||||||
|
assertThatThrownBy(() -> new YieldPercentage(0))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("calculateRequiredInput()")
|
||||||
|
class CalculateRequiredInput {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("80% yield: 100kg output requires 125kg input")
|
||||||
|
void should_Calculate125kg_When_80PercentYieldAnd100kg() {
|
||||||
|
var yield80 = new YieldPercentage(80);
|
||||||
|
var output = Quantity.of(new BigDecimal("100"), KILOGRAM).unsafeGetValue();
|
||||||
|
|
||||||
|
var input = yield80.calculateRequiredInput(output);
|
||||||
|
|
||||||
|
assertThat(input.amount()).isEqualByComparingTo(new BigDecimal("125"));
|
||||||
|
assertThat(input.uom()).isEqualTo(KILOGRAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("100% yield: input equals output")
|
||||||
|
void should_ReturnSameAmount_When_100PercentYield() {
|
||||||
|
var yield100 = new YieldPercentage(100);
|
||||||
|
var output = Quantity.of(new BigDecimal("50"), LITER).unsafeGetValue();
|
||||||
|
|
||||||
|
var input = yield100.calculateRequiredInput(output);
|
||||||
|
|
||||||
|
assertThat(input.amount()).isEqualByComparingTo(new BigDecimal("50"));
|
||||||
|
assertThat(input.uom()).isEqualTo(LITER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("50% yield: 100kg output requires 200kg input")
|
||||||
|
void should_Calculate200kg_When_50PercentYieldAnd100kg() {
|
||||||
|
var yield50 = new YieldPercentage(50);
|
||||||
|
var output = Quantity.of(new BigDecimal("100"), KILOGRAM).unsafeGetValue();
|
||||||
|
|
||||||
|
var input = yield50.calculateRequiredInput(output);
|
||||||
|
|
||||||
|
assertThat(input.amount()).isEqualByComparingTo(new BigDecimal("200"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should handle dual quantities")
|
||||||
|
void should_HandleDualQuantity() {
|
||||||
|
var yield80 = new YieldPercentage(80);
|
||||||
|
var output = Quantity.dual(
|
||||||
|
new BigDecimal("10"), PIECE,
|
||||||
|
new BigDecimal("20"), KILOGRAM).unsafeGetValue();
|
||||||
|
|
||||||
|
var input = yield80.calculateRequiredInput(output);
|
||||||
|
|
||||||
|
assertThat(input.amount()).isEqualByComparingTo(new BigDecimal("12.5"));
|
||||||
|
assertThat(input.secondaryAmount()).isEqualByComparingTo(new BigDecimal("25"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("asFactor()")
|
||||||
|
class AsFactor {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("should return decimal factor")
|
||||||
|
void should_ReturnDecimalFactor() {
|
||||||
|
var yield80 = new YieldPercentage(80);
|
||||||
|
assertThat(yield80.asFactor()).isEqualByComparingTo(new BigDecimal("0.8"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue