1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 19:40:18 +01:00

feat(inventory): Charge einbuchen (addBatch) (#5)

StockBatch als Child-Entity im Stock-Aggregat mit BatchReference
(batchId + batchType), Quantity, ExpiryDate und Status AVAILABLE.
POST /api/inventory/stocks/{stockId}/batches → 201.
This commit is contained in:
Sebastian Frick 2026-02-19 22:26:24 +01:00
parent 5224001dd7
commit 6feb3a9f1c
26 changed files with 1325 additions and 10 deletions

View file

@ -0,0 +1,53 @@
package de.effigenix.application.inventory;
import de.effigenix.application.inventory.command.AddStockBatchCommand;
import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.Result;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public class AddStockBatch {
private final StockRepository stockRepository;
public AddStockBatch(StockRepository stockRepository) {
this.stockRepository = stockRepository;
}
public Result<StockError, StockBatch> execute(AddStockBatchCommand cmd) {
// 1. Stock laden
Stock stock;
switch (stockRepository.findById(StockId.of(cmd.stockId()))) {
case Result.Failure(var err) ->
{ return Result.failure(new StockError.RepositoryFailure(err.message())); }
case Result.Success(var opt) -> {
if (opt.isEmpty()) {
return Result.failure(new StockError.StockNotFound(cmd.stockId()));
}
stock = opt.get();
}
}
// 2. Batch hinzufügen (Domain validiert)
var draft = new StockBatchDraft(
cmd.batchId(), cmd.batchType(),
cmd.quantityAmount(), cmd.quantityUnit(),
cmd.expiryDate()
);
StockBatch batch;
switch (stock.addBatch(draft)) {
case Result.Failure(var err) -> { return Result.failure(err); }
case Result.Success(var val) -> batch = val;
}
// 3. Stock speichern
switch (stockRepository.save(stock)) {
case Result.Failure(var err) ->
{ return Result.failure(new StockError.RepositoryFailure(err.message())); }
case Result.Success(var ignored) -> { }
}
return Result.success(batch);
}
}

View file

@ -0,0 +1,10 @@
package de.effigenix.application.inventory.command;
public record AddStockBatchCommand(
String stockId,
String batchId,
String batchType,
String quantityAmount,
String quantityUnit,
String expiryDate
) {}

View file

@ -0,0 +1,29 @@
package de.effigenix.domain.inventory;
import de.effigenix.shared.common.Result;
import java.util.Objects;
public record BatchReference(String batchId, BatchType batchType) {
public BatchReference {
Objects.requireNonNull(batchId, "batchId must not be null");
Objects.requireNonNull(batchType, "batchType must not be null");
}
public static Result<StockError, BatchReference> of(String batchId, String batchType) {
if (batchId == null || batchId.isBlank()) {
return Result.failure(new StockError.InvalidBatchReference("batchId must not be blank"));
}
BatchType type;
try {
type = BatchType.valueOf(batchType);
} catch (IllegalArgumentException | NullPointerException e) {
return Result.failure(new StockError.InvalidBatchReference(
"Invalid batchType: " + batchType + ". Allowed values: PRODUCED, PURCHASED"));
}
return Result.success(new BatchReference(batchId, type));
}
}

View file

@ -0,0 +1,5 @@
package de.effigenix.domain.inventory;
public enum BatchType {
PRODUCED, PURCHASED
}

View file

@ -3,6 +3,9 @@ package de.effigenix.domain.inventory;
import de.effigenix.domain.masterdata.ArticleId;
import de.effigenix.shared.common.Result;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
@ -12,6 +15,7 @@ import java.util.Objects;
* - ArticleId + StorageLocationId = unique (Application Layer, Repository-Concern)
* - MinimumLevel optional (quantity amount >= 0)
* - MinimumShelfLife optional (days > 0)
* - BatchReference (batchId + batchType) unique within batches
*/
public class Stock {
@ -20,19 +24,22 @@ public class Stock {
private final StorageLocationId storageLocationId;
private MinimumLevel minimumLevel;
private MinimumShelfLife minimumShelfLife;
private final List<StockBatch> batches;
private Stock(
StockId id,
ArticleId articleId,
StorageLocationId storageLocationId,
MinimumLevel minimumLevel,
MinimumShelfLife minimumShelfLife
MinimumShelfLife minimumShelfLife,
List<StockBatch> batches
) {
this.id = id;
this.articleId = articleId;
this.storageLocationId = storageLocationId;
this.minimumLevel = minimumLevel;
this.minimumShelfLife = minimumShelfLife;
this.batches = new ArrayList<>(batches);
}
/**
@ -81,7 +88,7 @@ public class Stock {
}
return Result.success(new Stock(
StockId.generate(), articleId, storageLocationId, minimumLevel, minimumShelfLife
StockId.generate(), articleId, storageLocationId, minimumLevel, minimumShelfLife, List.of()
));
}
@ -93,9 +100,28 @@ public class Stock {
ArticleId articleId,
StorageLocationId storageLocationId,
MinimumLevel minimumLevel,
MinimumShelfLife minimumShelfLife
MinimumShelfLife minimumShelfLife,
List<StockBatch> batches
) {
return new Stock(id, articleId, storageLocationId, minimumLevel, minimumShelfLife);
return new Stock(id, articleId, storageLocationId, minimumLevel, minimumShelfLife, batches);
}
// ==================== Batch Management ====================
public Result<StockError, StockBatch> addBatch(StockBatchDraft draft) {
StockBatch batch;
switch (StockBatch.create(draft)) {
case Result.Failure(var err) -> { return Result.failure(err); }
case Result.Success(var val) -> batch = val;
}
if (hasBatchReference(batch.batchReference())) {
return Result.failure(new StockError.DuplicateBatchReference(
batch.batchReference().batchId(), batch.batchReference().batchType().name()));
}
this.batches.add(batch);
return Result.success(batch);
}
// ==================== Getters ====================
@ -105,6 +131,13 @@ public class Stock {
public StorageLocationId storageLocationId() { return storageLocationId; }
public MinimumLevel minimumLevel() { return minimumLevel; }
public MinimumShelfLife minimumShelfLife() { return minimumShelfLife; }
public List<StockBatch> batches() { return Collections.unmodifiableList(batches); }
// ==================== Helpers ====================
private boolean hasBatchReference(BatchReference ref) {
return batches.stream().anyMatch(b -> b.batchReference().equals(ref));
}
@Override
public boolean equals(Object obj) {

View file

@ -0,0 +1,115 @@
package de.effigenix.domain.inventory;
import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.Objects;
/**
* Child entity of Stock representing a single batch (Charge).
*
* Invariants:
* - BatchReference (batchId + batchType) must be valid
* - Quantity must be positive
* - ExpiryDate must be a valid ISO date
* - New batches always have status AVAILABLE
* - receivedAt is set automatically on creation
*/
public class StockBatch {
private final StockBatchId id;
private final BatchReference batchReference;
private final Quantity quantity;
private final LocalDate expiryDate;
private final StockBatchStatus status;
private final Instant receivedAt;
private StockBatch(StockBatchId id, BatchReference batchReference, Quantity quantity,
LocalDate expiryDate, StockBatchStatus status, Instant receivedAt) {
this.id = id;
this.batchReference = batchReference;
this.quantity = quantity;
this.expiryDate = expiryDate;
this.status = status;
this.receivedAt = receivedAt;
}
public static Result<StockError, StockBatch> create(StockBatchDraft draft) {
// 1. BatchReference validieren
BatchReference batchReference;
switch (BatchReference.of(draft.batchId(), draft.batchType())) {
case Result.Failure(var err) -> { return Result.failure(err); }
case Result.Success(var val) -> batchReference = val;
}
// 2. Quantity validieren (Pflicht, positiv)
Quantity quantity;
try {
BigDecimal amount = new BigDecimal(draft.quantityAmount());
UnitOfMeasure uom;
try {
uom = UnitOfMeasure.valueOf(draft.quantityUnit());
} catch (IllegalArgumentException | NullPointerException e) {
return Result.failure(new StockError.InvalidQuantity("Invalid unit: " + draft.quantityUnit()));
}
switch (Quantity.of(amount, uom)) {
case Result.Failure(var err) -> {
return Result.failure(new StockError.InvalidQuantity(err.message()));
}
case Result.Success(var val) -> quantity = val;
}
} catch (NumberFormatException | NullPointerException e) {
return Result.failure(new StockError.InvalidQuantity(
"Invalid quantity amount: " + draft.quantityAmount()));
}
// 3. ExpiryDate validieren (Pflicht)
LocalDate expiryDate;
try {
if (draft.expiryDate() == null || draft.expiryDate().isBlank()) {
return Result.failure(new StockError.InvalidExpiryDate("expiryDate must not be blank"));
}
expiryDate = LocalDate.parse(draft.expiryDate());
} catch (DateTimeParseException e) {
return Result.failure(new StockError.InvalidExpiryDate(
"Invalid ISO date: " + draft.expiryDate()));
}
return Result.success(new StockBatch(
StockBatchId.generate(), batchReference, quantity,
expiryDate, StockBatchStatus.AVAILABLE, Instant.now()
));
}
public static StockBatch reconstitute(StockBatchId id, BatchReference batchReference,
Quantity quantity, LocalDate expiryDate,
StockBatchStatus status, Instant receivedAt) {
return new StockBatch(id, batchReference, quantity, expiryDate, status, receivedAt);
}
// ==================== Getters ====================
public StockBatchId id() { return id; }
public BatchReference batchReference() { return batchReference; }
public Quantity quantity() { return quantity; }
public LocalDate expiryDate() { return expiryDate; }
public StockBatchStatus status() { return status; }
public Instant receivedAt() { return receivedAt; }
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof StockBatch other)) return false;
return Objects.equals(id, other.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}

View file

@ -0,0 +1,9 @@
package de.effigenix.domain.inventory;
public record StockBatchDraft(
String batchId,
String batchType,
String quantityAmount,
String quantityUnit,
String expiryDate
) {}

View file

@ -0,0 +1,20 @@
package de.effigenix.domain.inventory;
import java.util.UUID;
public record StockBatchId(String value) {
public StockBatchId {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("StockBatchId must not be blank");
}
}
public static StockBatchId generate() {
return new StockBatchId(UUID.randomUUID().toString());
}
public static StockBatchId of(String value) {
return new StockBatchId(value);
}
}

View file

@ -0,0 +1,5 @@
package de.effigenix.domain.inventory;
public enum StockBatchStatus {
AVAILABLE, EXPIRING_SOON, BLOCKED, EXPIRED
}

View file

@ -35,6 +35,31 @@ public sealed interface StockError {
@Override public String message() { return "Invalid storage location ID: " + reason; }
}
record InvalidBatchReference(String reason) implements StockError {
@Override public String code() { return "INVALID_BATCH_REFERENCE"; }
@Override public String message() { return "Invalid batch reference: " + reason; }
}
record DuplicateBatchReference(String batchId, String batchType) implements StockError {
@Override public String code() { return "DUPLICATE_BATCH_REFERENCE"; }
@Override public String message() { return "Batch reference already exists: " + batchId + "/" + batchType; }
}
record InvalidQuantity(String reason) implements StockError {
@Override public String code() { return "INVALID_QUANTITY"; }
@Override public String message() { return "Invalid quantity: " + reason; }
}
record InvalidExpiryDate(String reason) implements StockError {
@Override public String code() { return "INVALID_EXPIRY_DATE"; }
@Override public String message() { return "Invalid expiry date: " + reason; }
}
record BatchNotFound(String id) implements StockError {
@Override public String code() { return "BATCH_NOT_FOUND"; }
@Override public String message() { return "Batch not found: " + id; }
}
record Unauthorized(String message) implements StockError {
@Override public String code() { return "UNAUTHORIZED"; }
}

View file

@ -1,6 +1,7 @@
package de.effigenix.infrastructure.config;
import de.effigenix.application.inventory.ActivateStorageLocation;
import de.effigenix.application.inventory.AddStockBatch;
import de.effigenix.application.inventory.CreateStock;
import de.effigenix.application.inventory.CreateStorageLocation;
import de.effigenix.application.inventory.DeactivateStorageLocation;
@ -47,4 +48,9 @@ public class InventoryUseCaseConfiguration {
public CreateStock createStock(StockRepository stockRepository) {
return new CreateStock(stockRepository);
}
@Bean
public AddStockBatch addStockBatch(StockRepository stockRepository) {
return new AddStockBatch(stockRepository);
}
}

View file

@ -0,0 +1,77 @@
package de.effigenix.infrastructure.inventory.persistence.entity;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
@Entity
@Table(name = "stock_batches")
public class StockBatchEntity {
@Id
@Column(name = "id", nullable = false, length = 36)
private String id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "stock_id", nullable = false)
private StockEntity stock;
@Column(name = "batch_id", nullable = false, length = 100)
private String batchId;
@Column(name = "batch_type", nullable = false, length = 20)
private String batchType;
@Column(name = "quantity_amount", nullable = false, precision = 19, scale = 6)
private BigDecimal quantityAmount;
@Column(name = "quantity_unit", nullable = false, length = 20)
private String quantityUnit;
@Column(name = "expiry_date", nullable = false)
private LocalDate expiryDate;
@Column(name = "status", nullable = false, length = 20)
private String status;
@Column(name = "received_at", nullable = false)
private Instant receivedAt;
protected StockBatchEntity() {}
public StockBatchEntity(String id, StockEntity stock, String batchId, String batchType,
BigDecimal quantityAmount, String quantityUnit, LocalDate expiryDate,
String status, Instant receivedAt) {
this.id = id;
this.stock = stock;
this.batchId = batchId;
this.batchType = batchType;
this.quantityAmount = quantityAmount;
this.quantityUnit = quantityUnit;
this.expiryDate = expiryDate;
this.status = status;
this.receivedAt = receivedAt;
}
public String getId() { return id; }
public StockEntity getStock() { return stock; }
public String getBatchId() { return batchId; }
public String getBatchType() { return batchType; }
public BigDecimal getQuantityAmount() { return quantityAmount; }
public String getQuantityUnit() { return quantityUnit; }
public LocalDate getExpiryDate() { return expiryDate; }
public String getStatus() { return status; }
public Instant getReceivedAt() { return receivedAt; }
public void setId(String id) { this.id = id; }
public void setStock(StockEntity stock) { this.stock = stock; }
public void setBatchId(String batchId) { this.batchId = batchId; }
public void setBatchType(String batchType) { this.batchType = batchType; }
public void setQuantityAmount(BigDecimal quantityAmount) { this.quantityAmount = quantityAmount; }
public void setQuantityUnit(String quantityUnit) { this.quantityUnit = quantityUnit; }
public void setExpiryDate(LocalDate expiryDate) { this.expiryDate = expiryDate; }
public void setStatus(String status) { this.status = status; }
public void setReceivedAt(Instant receivedAt) { this.receivedAt = receivedAt; }
}

View file

@ -3,6 +3,8 @@ package de.effigenix.infrastructure.inventory.persistence.entity;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "stocks")
@ -27,6 +29,9 @@ public class StockEntity {
@Column(name = "minimum_shelf_life_days")
private Integer minimumShelfLifeDays;
@OneToMany(mappedBy = "stock", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
private List<StockBatchEntity> batches = new ArrayList<>();
public StockEntity() {}
// ==================== Getters & Setters ====================
@ -48,4 +53,7 @@ public class StockEntity {
public Integer getMinimumShelfLifeDays() { return minimumShelfLifeDays; }
public void setMinimumShelfLifeDays(Integer minimumShelfLifeDays) { this.minimumShelfLifeDays = minimumShelfLifeDays; }
public List<StockBatchEntity> getBatches() { return batches; }
public void setBatches(List<StockBatchEntity> batches) { this.batches = batches; }
}

View file

@ -4,9 +4,13 @@ import de.effigenix.domain.inventory.*;
import de.effigenix.domain.masterdata.ArticleId;
import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.UnitOfMeasure;
import de.effigenix.infrastructure.inventory.persistence.entity.StockBatchEntity;
import de.effigenix.infrastructure.inventory.persistence.entity.StockEntity;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class StockMapper {
@ -25,6 +29,11 @@ public class StockMapper {
entity.setMinimumShelfLifeDays(stock.minimumShelfLife().days());
}
List<StockBatchEntity> batchEntities = stock.batches().stream()
.map(b -> toBatchEntity(b, entity))
.collect(Collectors.toList());
entity.setBatches(batchEntities);
return entity;
}
@ -44,12 +53,46 @@ public class StockMapper {
minimumShelfLife = new MinimumShelfLife(entity.getMinimumShelfLifeDays());
}
List<StockBatch> batches = entity.getBatches().stream()
.map(this::toDomainBatch)
.collect(Collectors.toList());
return Stock.reconstitute(
StockId.of(entity.getId()),
ArticleId.of(entity.getArticleId()),
StorageLocationId.of(entity.getStorageLocationId()),
minimumLevel,
minimumShelfLife
minimumShelfLife,
batches
);
}
private StockBatchEntity toBatchEntity(StockBatch batch, StockEntity stockEntity) {
return new StockBatchEntity(
batch.id().value(),
stockEntity,
batch.batchReference().batchId(),
batch.batchReference().batchType().name(),
batch.quantity().amount(),
batch.quantity().uom().name(),
batch.expiryDate(),
batch.status().name(),
batch.receivedAt()
);
}
private StockBatch toDomainBatch(StockBatchEntity entity) {
return StockBatch.reconstitute(
StockBatchId.of(entity.getId()),
new BatchReference(entity.getBatchId(), BatchType.valueOf(entity.getBatchType())),
Quantity.reconstitute(
entity.getQuantityAmount(),
UnitOfMeasure.valueOf(entity.getQuantityUnit()),
null, null
),
entity.getExpiryDate(),
StockBatchStatus.valueOf(entity.getStatus()),
entity.getReceivedAt()
);
}
}

View file

@ -1,9 +1,13 @@
package de.effigenix.infrastructure.inventory.web.controller;
import de.effigenix.application.inventory.AddStockBatch;
import de.effigenix.application.inventory.CreateStock;
import de.effigenix.application.inventory.command.AddStockBatchCommand;
import de.effigenix.application.inventory.command.CreateStockCommand;
import de.effigenix.domain.inventory.StockError;
import de.effigenix.infrastructure.inventory.web.dto.AddStockBatchRequest;
import de.effigenix.infrastructure.inventory.web.dto.CreateStockRequest;
import de.effigenix.infrastructure.inventory.web.dto.StockBatchResponse;
import de.effigenix.infrastructure.inventory.web.dto.StockResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -25,9 +29,11 @@ public class StockController {
private static final Logger logger = LoggerFactory.getLogger(StockController.class);
private final CreateStock createStock;
private final AddStockBatch addStockBatch;
public StockController(CreateStock createStock) {
public StockController(CreateStock createStock, AddStockBatch addStockBatch) {
this.createStock = createStock;
this.addStockBatch = addStockBatch;
}
@PostMapping
@ -55,6 +61,31 @@ public class StockController {
.body(StockResponse.from(result.unsafeGetValue()));
}
@PostMapping("/{stockId}/batches")
@PreAuthorize("hasAuthority('STOCK_WRITE')")
public ResponseEntity<StockBatchResponse> addBatch(
@PathVariable String stockId,
@Valid @RequestBody AddStockBatchRequest request,
Authentication authentication
) {
logger.info("Adding batch to stock: {} by actor: {}", stockId, authentication.getName());
var cmd = new AddStockBatchCommand(
stockId, request.batchId(), request.batchType(),
request.quantityAmount(), request.quantityUnit(),
request.expiryDate()
);
var result = addStockBatch.execute(cmd);
if (result.isFailure()) {
throw new StockDomainErrorException(result.unsafeGetError());
}
logger.info("Batch added: {}", result.unsafeGetValue().id().value());
return ResponseEntity.status(HttpStatus.CREATED)
.body(StockBatchResponse.from(result.unsafeGetValue()));
}
public static class StockDomainErrorException extends RuntimeException {
private final StockError error;

View file

@ -0,0 +1,11 @@
package de.effigenix.infrastructure.inventory.web.dto;
import jakarta.validation.constraints.NotBlank;
public record AddStockBatchRequest(
@NotBlank String batchId,
@NotBlank String batchType,
@NotBlank String quantityAmount,
@NotBlank String quantityUnit,
@NotBlank String expiryDate
) {}

View file

@ -0,0 +1,32 @@
package de.effigenix.infrastructure.inventory.web.dto;
import de.effigenix.domain.inventory.StockBatch;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
public record StockBatchResponse(
String id,
String batchId,
String batchType,
BigDecimal quantityAmount,
String quantityUnit,
LocalDate expiryDate,
String status,
Instant receivedAt
) {
public static StockBatchResponse from(StockBatch batch) {
return new StockBatchResponse(
batch.id().value(),
batch.batchReference().batchId(),
batch.batchReference().batchType().name(),
batch.quantity().amount(),
batch.quantity().uom().name(),
batch.expiryDate(),
batch.status().name(),
batch.receivedAt()
);
}
}

View file

@ -25,11 +25,16 @@ public final class InventoryErrorHttpStatusMapper {
public static int toHttpStatus(StockError error) {
return switch (error) {
case StockError.StockNotFound e -> 404;
case StockError.BatchNotFound e -> 404;
case StockError.DuplicateStock e -> 409;
case StockError.DuplicateBatchReference e -> 409;
case StockError.InvalidMinimumLevel e -> 400;
case StockError.InvalidMinimumShelfLife e -> 400;
case StockError.InvalidArticleId e -> 400;
case StockError.InvalidStorageLocationId e -> 400;
case StockError.InvalidBatchReference e -> 400;
case StockError.InvalidQuantity e -> 400;
case StockError.InvalidExpiryDate e -> 400;
case StockError.Unauthorized e -> 403;
case StockError.RepositoryFailure e -> 500;
};

View file

@ -0,0 +1,41 @@
package de.effigenix.infrastructure.stub;
import de.effigenix.domain.inventory.Stock;
import de.effigenix.domain.inventory.StockId;
import de.effigenix.domain.inventory.StockRepository;
import de.effigenix.domain.inventory.StorageLocationId;
import de.effigenix.domain.masterdata.ArticleId;
import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
@Profile("no-db")
public class StubStockRepository implements StockRepository {
private static final RepositoryError.DatabaseError STUB_ERROR =
new RepositoryError.DatabaseError("Stub-Modus: keine Datenbankverbindung");
@Override
public Result<RepositoryError, Optional<Stock>> findById(StockId id) {
return Result.failure(STUB_ERROR);
}
@Override
public Result<RepositoryError, Optional<Stock>> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) {
return Result.failure(STUB_ERROR);
}
@Override
public Result<RepositoryError, Boolean> existsByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) {
return Result.failure(STUB_ERROR);
}
@Override
public Result<RepositoryError, Void> save(Stock stock) {
return Result.failure(STUB_ERROR);
}
}

View file

@ -0,0 +1,62 @@
<?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="014-create-stock-batches-table" author="effigenix">
<createTable tableName="stock_batches">
<column name="id" type="VARCHAR(36)">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="stock_id" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="batch_id" type="VARCHAR(100)">
<constraints nullable="false"/>
</column>
<column name="batch_type" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="quantity_amount" type="DECIMAL(19,6)">
<constraints nullable="false"/>
</column>
<column name="quantity_unit" type="VARCHAR(20)">
<constraints nullable="false"/>
</column>
<column name="expiry_date" type="DATE">
<constraints nullable="false"/>
</column>
<column name="status" type="VARCHAR(20)" defaultValue="AVAILABLE">
<constraints nullable="false"/>
</column>
<column name="received_at" type="TIMESTAMP WITH TIME ZONE">
<constraints nullable="false"/>
</column>
</createTable>
<addForeignKeyConstraint baseTableName="stock_batches" baseColumnNames="stock_id"
referencedTableName="stocks" referencedColumnNames="id"
constraintName="fk_stock_batches_stock"
onDelete="CASCADE"/>
<addUniqueConstraint tableName="stock_batches" columnNames="stock_id, batch_id, batch_type"
constraintName="uq_stock_batch_reference"/>
<createIndex tableName="stock_batches" indexName="idx_stock_batches_stock_id">
<column name="stock_id"/>
</createIndex>
<sql>
ALTER TABLE stock_batches ADD CONSTRAINT chk_stock_batch_type
CHECK (batch_type IN ('PRODUCED', 'PURCHASED'));
</sql>
<sql>
ALTER TABLE stock_batches ADD CONSTRAINT chk_stock_batch_status
CHECK (status IN ('AVAILABLE', 'EXPIRING_SOON', 'BLOCKED', 'EXPIRED'));
</sql>
</changeSet>
</databaseChangeLog>

View file

@ -18,5 +18,6 @@
<include file="db/changelog/changes/011-create-recipe-ingredients-table.xml"/>
<include file="db/changelog/changes/012-create-recipe-production-steps-table.xml"/>
<include file="db/changelog/changes/013-create-stock-schema.xml"/>
<include file="db/changelog/changes/014-create-stock-batches-table.xml"/>
</databaseChangeLog>