mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 17:39:57 +01:00
feat(inventory): Inventur abschließen mit Ausgleichsbuchungen (US-6.3)
Vier-Augen-Prinzip (completedBy ≠ initiatedBy), Vollständigkeitsprüfung
aller CountItems, und automatische ADJUSTMENT-StockMovements für
Abweichungen (IN bei Ist > Soll, OUT bei Ist < Soll).
Domain: complete()-Methode, InventoryCountReconciliationService
Application: CompleteInventoryCount UseCase
Infrastruktur: POST /{id}/complete Endpoint, Liquibase-Migration
Closes #19
This commit is contained in:
parent
6996a301f9
commit
e4f4537581
21 changed files with 1373 additions and 26 deletions
|
|
@ -0,0 +1,114 @@
|
|||
package de.effigenix.application.inventory;
|
||||
|
||||
import de.effigenix.application.inventory.command.CompleteInventoryCountCommand;
|
||||
import de.effigenix.domain.inventory.*;
|
||||
import de.effigenix.shared.common.Result;
|
||||
import de.effigenix.shared.persistence.UnitOfWork;
|
||||
import de.effigenix.shared.security.ActorId;
|
||||
import de.effigenix.shared.security.AuthorizationPort;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CompleteInventoryCount {
|
||||
|
||||
private final InventoryCountRepository inventoryCountRepository;
|
||||
private final StockRepository stockRepository;
|
||||
private final StockMovementRepository stockMovementRepository;
|
||||
private final InventoryCountReconciliationService reconciliationService;
|
||||
private final UnitOfWork unitOfWork;
|
||||
private final AuthorizationPort authPort;
|
||||
|
||||
public CompleteInventoryCount(InventoryCountRepository inventoryCountRepository,
|
||||
StockRepository stockRepository,
|
||||
StockMovementRepository stockMovementRepository,
|
||||
InventoryCountReconciliationService reconciliationService,
|
||||
UnitOfWork unitOfWork,
|
||||
AuthorizationPort authPort) {
|
||||
this.inventoryCountRepository = inventoryCountRepository;
|
||||
this.stockRepository = stockRepository;
|
||||
this.stockMovementRepository = stockMovementRepository;
|
||||
this.reconciliationService = reconciliationService;
|
||||
this.unitOfWork = unitOfWork;
|
||||
this.authPort = authPort;
|
||||
}
|
||||
|
||||
public Result<InventoryCountError, InventoryCount> execute(CompleteInventoryCountCommand cmd, ActorId actorId) {
|
||||
if (!authPort.can(actorId, InventoryAction.INVENTORY_COUNT_WRITE)) {
|
||||
return Result.failure(new InventoryCountError.Unauthorized("Not authorized to complete inventory counts"));
|
||||
}
|
||||
|
||||
if (cmd.inventoryCountId() == null || cmd.inventoryCountId().isBlank()) {
|
||||
return Result.failure(new InventoryCountError.InvalidInventoryCountId("must not be blank"));
|
||||
}
|
||||
|
||||
InventoryCountId id;
|
||||
try {
|
||||
id = InventoryCountId.of(cmd.inventoryCountId());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Result.failure(new InventoryCountError.InvalidInventoryCountId(e.getMessage()));
|
||||
}
|
||||
|
||||
// 1. InventoryCount laden
|
||||
InventoryCount count;
|
||||
switch (inventoryCountRepository.findById(id)) {
|
||||
case Result.Failure(var err) ->
|
||||
{ return Result.failure(new InventoryCountError.RepositoryFailure(err.message())); }
|
||||
case Result.Success(var optCount) -> {
|
||||
if (optCount.isEmpty()) {
|
||||
return Result.failure(new InventoryCountError.InventoryCountNotFound(cmd.inventoryCountId()));
|
||||
}
|
||||
count = optCount.get();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Inventur abschließen (Vier-Augen-Prinzip, Vollständigkeit)
|
||||
switch (count.complete(cmd.completedBy())) {
|
||||
case Result.Failure(var err) -> { return Result.failure(err); }
|
||||
case Result.Success(var ignored) -> { }
|
||||
}
|
||||
|
||||
// 3. Stocks für StorageLocation laden
|
||||
List<Stock> stocks;
|
||||
switch (stockRepository.findAllByStorageLocationId(count.storageLocationId())) {
|
||||
case Result.Failure(var err) ->
|
||||
{ return Result.failure(new InventoryCountError.RepositoryFailure(err.message())); }
|
||||
case Result.Success(var val) -> stocks = val;
|
||||
}
|
||||
|
||||
// 4. Reconciliation: Ausgleichsbuchungen erzeugen
|
||||
List<StockMovementDraft> movementDrafts;
|
||||
switch (reconciliationService.reconcile(count, stocks, cmd.completedBy())) {
|
||||
case Result.Failure(var err) -> { return Result.failure(err); }
|
||||
case Result.Success(var val) -> movementDrafts = val;
|
||||
}
|
||||
|
||||
// 5. StockMovements erzeugen und validieren
|
||||
List<StockMovement> movements = new java.util.ArrayList<>();
|
||||
for (StockMovementDraft draft : movementDrafts) {
|
||||
switch (StockMovement.record(draft)) {
|
||||
case Result.Failure(var err) ->
|
||||
{ return Result.failure(new InventoryCountError.RepositoryFailure(err.message())); }
|
||||
case Result.Success(var val) -> movements.add(val);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Atomar speichern
|
||||
return unitOfWork.executeAtomically(() -> {
|
||||
switch (inventoryCountRepository.save(count)) {
|
||||
case Result.Failure(var err) ->
|
||||
{ return Result.failure(new InventoryCountError.RepositoryFailure(err.message())); }
|
||||
case Result.Success(var ignored) -> { }
|
||||
}
|
||||
|
||||
for (StockMovement movement : movements) {
|
||||
switch (stockMovementRepository.save(movement)) {
|
||||
case Result.Failure(var err) ->
|
||||
{ return Result.failure(new InventoryCountError.RepositoryFailure(err.message())); }
|
||||
case Result.Success(var ignored) -> { }
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success(count);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package de.effigenix.application.inventory.command;
|
||||
|
||||
public record CompleteInventoryCountCommand(
|
||||
String inventoryCountId,
|
||||
String completedBy
|
||||
) {}
|
||||
|
|
@ -25,6 +25,7 @@ import java.util.Objects;
|
|||
* - addCountItem only in status OPEN
|
||||
* - startCounting only in status OPEN, requires non-empty countItems
|
||||
* - updateCountItem only in status COUNTING
|
||||
* - complete only in status COUNTING, requires all items counted, Vier-Augen-Prinzip (completedBy ≠ initiatedBy)
|
||||
*/
|
||||
public class InventoryCount {
|
||||
|
||||
|
|
@ -32,6 +33,7 @@ public class InventoryCount {
|
|||
private final StorageLocationId storageLocationId;
|
||||
private final LocalDate countDate;
|
||||
private final String initiatedBy;
|
||||
private String completedBy;
|
||||
private InventoryCountStatus status;
|
||||
private final Instant createdAt;
|
||||
private final List<CountItem> countItems;
|
||||
|
|
@ -41,6 +43,7 @@ public class InventoryCount {
|
|||
StorageLocationId storageLocationId,
|
||||
LocalDate countDate,
|
||||
String initiatedBy,
|
||||
String completedBy,
|
||||
InventoryCountStatus status,
|
||||
Instant createdAt,
|
||||
List<CountItem> countItems
|
||||
|
|
@ -49,6 +52,7 @@ public class InventoryCount {
|
|||
this.storageLocationId = storageLocationId;
|
||||
this.countDate = countDate;
|
||||
this.initiatedBy = initiatedBy;
|
||||
this.completedBy = completedBy;
|
||||
this.status = status;
|
||||
this.createdAt = createdAt;
|
||||
this.countItems = new ArrayList<>(countItems);
|
||||
|
|
@ -90,7 +94,7 @@ public class InventoryCount {
|
|||
|
||||
return Result.success(new InventoryCount(
|
||||
InventoryCountId.generate(), storageLocationId, countDate,
|
||||
draft.initiatedBy(), InventoryCountStatus.OPEN, Instant.now(), List.of()
|
||||
draft.initiatedBy(), null, InventoryCountStatus.OPEN, Instant.now(), List.of()
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -102,11 +106,12 @@ public class InventoryCount {
|
|||
StorageLocationId storageLocationId,
|
||||
LocalDate countDate,
|
||||
String initiatedBy,
|
||||
String completedBy,
|
||||
InventoryCountStatus status,
|
||||
Instant createdAt,
|
||||
List<CountItem> countItems
|
||||
) {
|
||||
return new InventoryCount(id, storageLocationId, countDate, initiatedBy, status, createdAt, countItems);
|
||||
return new InventoryCount(id, storageLocationId, countDate, initiatedBy, completedBy, status, createdAt, countItems);
|
||||
}
|
||||
|
||||
// ==================== Count Item Management ====================
|
||||
|
|
@ -145,6 +150,29 @@ public class InventoryCount {
|
|||
return Result.success(null);
|
||||
}
|
||||
|
||||
public Result<InventoryCountError, Void> complete(String completedBy) {
|
||||
if (status != InventoryCountStatus.COUNTING) {
|
||||
return Result.failure(new InventoryCountError.InvalidStatusTransition(
|
||||
status.name(), InventoryCountStatus.COMPLETED.name()));
|
||||
}
|
||||
if (completedBy == null || completedBy.isBlank()) {
|
||||
return Result.failure(new InventoryCountError.InvalidInitiatedBy("completedBy must not be blank"));
|
||||
}
|
||||
if (countItems.isEmpty()) {
|
||||
return Result.failure(new InventoryCountError.NoCountItems());
|
||||
}
|
||||
boolean allCounted = countItems.stream().allMatch(CountItem::isCounted);
|
||||
if (!allCounted) {
|
||||
return Result.failure(new InventoryCountError.IncompleteCountItems());
|
||||
}
|
||||
if (completedBy.equals(initiatedBy)) {
|
||||
return Result.failure(new InventoryCountError.SamePersonViolation());
|
||||
}
|
||||
this.completedBy = completedBy;
|
||||
this.status = InventoryCountStatus.COMPLETED;
|
||||
return Result.success(null);
|
||||
}
|
||||
|
||||
public Result<InventoryCountError, Void> updateCountItem(CountItemId itemId, Quantity actualQuantity) {
|
||||
if (status != InventoryCountStatus.COUNTING) {
|
||||
return Result.failure(new InventoryCountError.InvalidStatusTransition(
|
||||
|
|
@ -187,6 +215,7 @@ public class InventoryCount {
|
|||
public StorageLocationId storageLocationId() { return storageLocationId; }
|
||||
public LocalDate countDate() { return countDate; }
|
||||
public String initiatedBy() { return initiatedBy; }
|
||||
public String completedBy() { return completedBy; }
|
||||
public InventoryCountStatus status() { return status; }
|
||||
public Instant createdAt() { return createdAt; }
|
||||
public List<CountItem> countItems() { return Collections.unmodifiableList(countItems); }
|
||||
|
|
|
|||
|
|
@ -85,6 +85,11 @@ public sealed interface InventoryCountError {
|
|||
@Override public String message() { return "An active inventory count already exists for storage location: " + storageLocationId; }
|
||||
}
|
||||
|
||||
record StockNotFoundForArticle(String articleId) implements InventoryCountError {
|
||||
@Override public String code() { return "STOCK_NOT_FOUND_FOR_ARTICLE"; }
|
||||
@Override public String message() { return "No stock found for article: " + articleId; }
|
||||
}
|
||||
|
||||
record Unauthorized(String message) implements InventoryCountError {
|
||||
@Override public String code() { return "UNAUTHORIZED"; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
package de.effigenix.domain.inventory;
|
||||
|
||||
import de.effigenix.domain.masterdata.article.ArticleId;
|
||||
import de.effigenix.shared.common.Result;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Domain Service: Erzeugt ADJUSTMENT-StockMovementDrafts für Inventur-Abweichungen.
|
||||
*
|
||||
* Für jedes CountItem mit Abweichung (actual ≠ expected) wird ein StockMovementDraft erzeugt:
|
||||
* - Positive Abweichung (Ist > Soll) → Direction IN
|
||||
* - Negative Abweichung (Ist < Soll) → Direction OUT
|
||||
*
|
||||
* Wählt die erste verfügbare Charge des jeweiligen Stocks als Buchungsziel.
|
||||
*/
|
||||
public class InventoryCountReconciliationService {
|
||||
|
||||
public Result<InventoryCountError, List<StockMovementDraft>> reconcile(
|
||||
InventoryCount count,
|
||||
List<Stock> stocks,
|
||||
String performedBy
|
||||
) {
|
||||
Map<ArticleId, Stock> stocksByArticle = stocks.stream()
|
||||
.collect(Collectors.toMap(Stock::articleId, s -> s, (a, b) -> a));
|
||||
|
||||
List<StockMovementDraft> drafts = new ArrayList<>();
|
||||
|
||||
for (CountItem item : count.countItems()) {
|
||||
BigDecimal deviation = item.deviation();
|
||||
if (deviation == null || deviation.compareTo(BigDecimal.ZERO) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Stock stock = stocksByArticle.get(item.articleId());
|
||||
if (stock == null || stock.batches().isEmpty()) {
|
||||
return Result.failure(new InventoryCountError.StockNotFoundForArticle(item.articleId().value()));
|
||||
}
|
||||
|
||||
StockBatch batch = stock.batches().getFirst();
|
||||
|
||||
String direction = deviation.compareTo(BigDecimal.ZERO) > 0 ? "IN" : "OUT";
|
||||
BigDecimal absAmount = deviation.abs();
|
||||
|
||||
drafts.add(new StockMovementDraft(
|
||||
stock.id().value(),
|
||||
item.articleId().value(),
|
||||
batch.id().value(),
|
||||
batch.batchReference().batchId(),
|
||||
batch.batchReference().batchType().name(),
|
||||
"ADJUSTMENT",
|
||||
direction,
|
||||
absAmount.toPlainString(),
|
||||
item.expectedQuantity().uom().name(),
|
||||
"Inventur-Ausgleich: Inventur " + count.id().value(),
|
||||
null,
|
||||
performedBy
|
||||
));
|
||||
}
|
||||
|
||||
return Result.success(drafts);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package de.effigenix.infrastructure.config;
|
||||
|
||||
import de.effigenix.application.inventory.CompleteInventoryCount;
|
||||
import de.effigenix.application.inventory.CreateInventoryCount;
|
||||
import de.effigenix.application.inventory.GetInventoryCount;
|
||||
import de.effigenix.application.inventory.ListInventoryCounts;
|
||||
|
|
@ -28,6 +29,7 @@ import de.effigenix.application.inventory.GetStorageLocation;
|
|||
import de.effigenix.application.inventory.ListStorageLocations;
|
||||
import de.effigenix.application.inventory.UpdateStorageLocation;
|
||||
import de.effigenix.application.usermanagement.AuditLogger;
|
||||
import de.effigenix.domain.inventory.InventoryCountReconciliationService;
|
||||
import de.effigenix.domain.inventory.InventoryCountRepository;
|
||||
import de.effigenix.domain.inventory.StockMovementRepository;
|
||||
import de.effigenix.domain.inventory.StockRepository;
|
||||
|
|
@ -184,4 +186,19 @@ public class InventoryUseCaseConfiguration {
|
|||
public RecordCountItem recordCountItem(InventoryCountRepository inventoryCountRepository, UnitOfWork unitOfWork, AuthorizationPort authorizationPort) {
|
||||
return new RecordCountItem(inventoryCountRepository, unitOfWork, authorizationPort);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public InventoryCountReconciliationService inventoryCountReconciliationService() {
|
||||
return new InventoryCountReconciliationService();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CompleteInventoryCount completeInventoryCount(InventoryCountRepository inventoryCountRepository,
|
||||
StockRepository stockRepository,
|
||||
StockMovementRepository stockMovementRepository,
|
||||
InventoryCountReconciliationService reconciliationService,
|
||||
UnitOfWork unitOfWork,
|
||||
AuthorizationPort authorizationPort) {
|
||||
return new CompleteInventoryCount(inventoryCountRepository, stockRepository, stockMovementRepository, reconciliationService, unitOfWork, authorizationPort);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,7 +101,8 @@ public class JdbcInventoryCountRepository implements InventoryCountRepository {
|
|||
int rows = jdbc.sql("""
|
||||
UPDATE inventory_counts
|
||||
SET storage_location_id = :storageLocationId, count_date = :countDate,
|
||||
initiated_by = :initiatedBy, status = :status, created_at = :createdAt
|
||||
initiated_by = :initiatedBy, completed_by = :completedBy,
|
||||
status = :status, created_at = :createdAt
|
||||
WHERE id = :id
|
||||
""")
|
||||
.param("id", inventoryCount.id().value())
|
||||
|
|
@ -110,8 +111,8 @@ public class JdbcInventoryCountRepository implements InventoryCountRepository {
|
|||
|
||||
if (rows == 0) {
|
||||
jdbc.sql("""
|
||||
INSERT INTO inventory_counts (id, storage_location_id, count_date, initiated_by, status, created_at)
|
||||
VALUES (:id, :storageLocationId, :countDate, :initiatedBy, :status, :createdAt)
|
||||
INSERT INTO inventory_counts (id, storage_location_id, count_date, initiated_by, completed_by, status, created_at)
|
||||
VALUES (:id, :storageLocationId, :countDate, :initiatedBy, :completedBy, :status, :createdAt)
|
||||
""")
|
||||
.param("id", inventoryCount.id().value())
|
||||
.params(countParams(inventoryCount))
|
||||
|
|
@ -134,6 +135,7 @@ public class JdbcInventoryCountRepository implements InventoryCountRepository {
|
|||
params.put("storageLocationId", count.storageLocationId().value());
|
||||
params.put("countDate", count.countDate());
|
||||
params.put("initiatedBy", count.initiatedBy());
|
||||
params.put("completedBy", count.completedBy());
|
||||
params.put("status", count.status().name());
|
||||
params.put("createdAt", count.createdAt().atOffset(ZoneOffset.UTC));
|
||||
return params;
|
||||
|
|
@ -206,7 +208,7 @@ public class JdbcInventoryCountRepository implements InventoryCountRepository {
|
|||
|
||||
return InventoryCount.reconstitute(
|
||||
count.id(), count.storageLocationId(), count.countDate(),
|
||||
count.initiatedBy(), count.status(), count.createdAt(), items
|
||||
count.initiatedBy(), count.completedBy(), count.status(), count.createdAt(), items
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -224,6 +226,7 @@ public class JdbcInventoryCountRepository implements InventoryCountRepository {
|
|||
StorageLocationId.of(rs.getString("storage_location_id")),
|
||||
rs.getObject("count_date", LocalDate.class),
|
||||
rs.getString("initiated_by"),
|
||||
rs.getString("completed_by"),
|
||||
InventoryCountStatus.valueOf(rs.getString("status")),
|
||||
rs.getObject("created_at", OffsetDateTime.class).toInstant(),
|
||||
List.of()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
package de.effigenix.infrastructure.inventory.web.controller;
|
||||
|
||||
import de.effigenix.application.inventory.CompleteInventoryCount;
|
||||
import de.effigenix.application.inventory.CreateInventoryCount;
|
||||
import de.effigenix.application.inventory.GetInventoryCount;
|
||||
import de.effigenix.application.inventory.ListInventoryCounts;
|
||||
import de.effigenix.application.inventory.RecordCountItem;
|
||||
import de.effigenix.application.inventory.StartInventoryCount;
|
||||
import de.effigenix.application.inventory.command.CompleteInventoryCountCommand;
|
||||
import de.effigenix.application.inventory.command.CreateInventoryCountCommand;
|
||||
import de.effigenix.application.inventory.command.RecordCountItemCommand;
|
||||
import de.effigenix.domain.inventory.InventoryCountError;
|
||||
|
|
@ -34,17 +36,20 @@ public class InventoryCountController {
|
|||
private final ListInventoryCounts listInventoryCounts;
|
||||
private final StartInventoryCount startInventoryCount;
|
||||
private final RecordCountItem recordCountItem;
|
||||
private final CompleteInventoryCount completeInventoryCount;
|
||||
|
||||
public InventoryCountController(CreateInventoryCount createInventoryCount,
|
||||
GetInventoryCount getInventoryCount,
|
||||
ListInventoryCounts listInventoryCounts,
|
||||
StartInventoryCount startInventoryCount,
|
||||
RecordCountItem recordCountItem) {
|
||||
RecordCountItem recordCountItem,
|
||||
CompleteInventoryCount completeInventoryCount) {
|
||||
this.createInventoryCount = createInventoryCount;
|
||||
this.getInventoryCount = getInventoryCount;
|
||||
this.listInventoryCounts = listInventoryCounts;
|
||||
this.startInventoryCount = startInventoryCount;
|
||||
this.recordCountItem = recordCountItem;
|
||||
this.completeInventoryCount = completeInventoryCount;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
|
|
@ -135,6 +140,22 @@ public class InventoryCountController {
|
|||
return ResponseEntity.ok(InventoryCountResponse.from(result.unsafeGetValue()));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/complete")
|
||||
@PreAuthorize("hasAuthority('INVENTORY_COUNT_WRITE')")
|
||||
public ResponseEntity<InventoryCountResponse> completeInventoryCount(
|
||||
@PathVariable String id,
|
||||
Authentication authentication
|
||||
) {
|
||||
var cmd = new CompleteInventoryCountCommand(id, authentication.getName());
|
||||
var result = completeInventoryCount.execute(cmd, ActorId.of(authentication.getName()));
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new InventoryCountDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(InventoryCountResponse.from(result.unsafeGetValue()));
|
||||
}
|
||||
|
||||
// ==================== Exception Wrapper ====================
|
||||
|
||||
public static class InventoryCountDomainErrorException extends RuntimeException {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ public record InventoryCountResponse(
|
|||
String storageLocationId,
|
||||
LocalDate countDate,
|
||||
String initiatedBy,
|
||||
String completedBy,
|
||||
String status,
|
||||
Instant createdAt,
|
||||
List<CountItemResponse> countItems
|
||||
|
|
@ -21,6 +22,7 @@ public record InventoryCountResponse(
|
|||
count.storageLocationId().value(),
|
||||
count.countDate(),
|
||||
count.initiatedBy(),
|
||||
count.completedBy(),
|
||||
count.status().name(),
|
||||
count.createdAt(),
|
||||
count.countItems().stream()
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ public final class InventoryErrorHttpStatusMapper {
|
|||
case InventoryCountError.NoCountItems e -> 409;
|
||||
case InventoryCountError.IncompleteCountItems e -> 409;
|
||||
case InventoryCountError.SamePersonViolation e -> 409;
|
||||
case InventoryCountError.StockNotFoundForArticle e -> 409;
|
||||
case InventoryCountError.CountDateInFuture e -> 400;
|
||||
case InventoryCountError.InvalidStorageLocationId e -> 400;
|
||||
case InventoryCountError.InvalidCountDate e -> 400;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
<?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="037-add-completed-by-to-inventory-counts" author="effigenix">
|
||||
<addColumn tableName="inventory_counts">
|
||||
<column name="completed_by" type="VARCHAR(36)">
|
||||
<constraints nullable="true"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
|
@ -42,5 +42,6 @@
|
|||
<include file="db/changelog/changes/034-create-inventory-counts-schema.xml"/>
|
||||
<include file="db/changelog/changes/035-seed-inventory-count-permissions.xml"/>
|
||||
<include file="db/changelog/changes/036-add-inventory-counts-composite-index.xml"/>
|
||||
<include file="db/changelog/changes/037-add-completed-by-to-inventory-counts.xml"/>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue