Workspace UI
This commit is contained in:
@@ -20,8 +20,9 @@ public class InventoryMovement {
|
||||
@Column(nullable = false)
|
||||
private int delta;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private String reason; // PRODUCTION, SALE, RETURN, ADJUSTMENT
|
||||
private MovementReason reason;
|
||||
|
||||
private String reference;
|
||||
|
||||
@@ -33,12 +34,12 @@ public class InventoryMovement {
|
||||
public Long getId() { return id; }
|
||||
public Sku getSku() { return sku; }
|
||||
public int getDelta() { return delta; }
|
||||
public String getReason() { return reason; }
|
||||
public MovementReason getReason() { return reason; }
|
||||
public String getReference() { return reference; }
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
|
||||
public void setSku(Sku sku) { this.sku = sku; }
|
||||
public void setDelta(int delta) { this.delta = delta; }
|
||||
public void setReason(String reason) { this.reason = reason; }
|
||||
public void setReason(MovementReason reason) { this.reason = reason; }
|
||||
public void setReference(String reference) { this.reference = reference; }
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@ package com.voyage.workspace.inventory;
|
||||
public record InventoryMovementCreateRequest(
|
||||
Long skuId,
|
||||
int delta,
|
||||
String reason,
|
||||
MovementReason reason,
|
||||
String reference
|
||||
) {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.voyage.workspace.inventory;
|
||||
|
||||
public enum MovementReason {
|
||||
PRODUCTION,
|
||||
SALE,
|
||||
RETURN,
|
||||
ADJUSTMENT
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.voyage.workspace.orders;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Table(name = "orders")
|
||||
public class Order {
|
||||
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String customerRef;
|
||||
|
||||
private Instant createdAt = Instant.now();
|
||||
|
||||
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<OrderItem> items = new ArrayList<>();
|
||||
|
||||
public void addItem(OrderItem item) {
|
||||
items.add(item);
|
||||
item.setOrder(this);
|
||||
}
|
||||
|
||||
// getters/setters
|
||||
public Long getId() { return id; }
|
||||
public String getCustomerRef() { return customerRef; }
|
||||
public void setCustomerRef(String customerRef) { this.customerRef = customerRef; }
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
public List<OrderItem> getItems() { return items; }
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.voyage.workspace.orders;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/orders")
|
||||
public class OrderController {
|
||||
|
||||
private final OrderRepository orderRepo;
|
||||
private final OrderService orderService;
|
||||
|
||||
public OrderController(OrderRepository orderRepo, OrderService orderService) {
|
||||
this.orderRepo = orderRepo;
|
||||
this.orderService = orderService;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<?> create(@RequestBody OrderCreateRequest req) {
|
||||
try {
|
||||
return ResponseEntity.ok(orderService.create(req));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<Order> list() {
|
||||
return orderRepo.findAll();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<?> get(@PathVariable Long id) {
|
||||
return orderRepo.findById(id)
|
||||
.<ResponseEntity<?>>map(ResponseEntity::ok)
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.voyage.workspace.orders;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record OrderCreateRequest(
|
||||
String customerRef,
|
||||
List<OrderItemRequest> items
|
||||
) {}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.voyage.workspace.orders;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.voyage.workspace.products.Sku;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Entity
|
||||
@Table(name = "order_items")
|
||||
public class OrderItem {
|
||||
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
|
||||
|
||||
@ManyToOne(optional = false, fetch = FetchType.EAGER)
|
||||
private Sku sku;
|
||||
|
||||
private int qty;
|
||||
|
||||
@Column(precision = 10, scale = 2)
|
||||
private BigDecimal unitPrice;
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "order_id")
|
||||
@JsonIgnore
|
||||
private Order order;
|
||||
|
||||
// getters/setters
|
||||
public Long getId() { return id; }
|
||||
public Order getOrder() { return order; }
|
||||
public void setOrder(Order order) { this.order = order; }
|
||||
public Sku getSku() { return sku; }
|
||||
public void setSku(Sku sku) { this.sku = sku; }
|
||||
public int getQty() { return qty; }
|
||||
public void setQty(int qty) { this.qty = qty; }
|
||||
public BigDecimal getUnitPrice() { return unitPrice; }
|
||||
public void setUnitPrice(BigDecimal unitPrice) { this.unitPrice = unitPrice; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.voyage.workspace.orders;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public record OrderItemRequest(
|
||||
Long skuId,
|
||||
int qty,
|
||||
BigDecimal unitPrice
|
||||
) {}
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
package com.voyage.workspace.orders;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface OrderRepository extends JpaRepository<Order, Long> {}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.voyage.workspace.orders;
|
||||
|
||||
import com.voyage.workspace.inventory.InventoryMovement;
|
||||
import com.voyage.workspace.inventory.InventoryMovementRepository;
|
||||
import com.voyage.workspace.inventory.MovementReason;
|
||||
import com.voyage.workspace.products.SkuRepository;
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class OrderService {
|
||||
|
||||
private final OrderRepository orderRepo;
|
||||
private final SkuRepository skuRepo;
|
||||
private final InventoryMovementRepository movementRepo;
|
||||
|
||||
public OrderService(OrderRepository orderRepo, SkuRepository skuRepo, InventoryMovementRepository movementRepo) {
|
||||
this.orderRepo = orderRepo;
|
||||
this.skuRepo = skuRepo;
|
||||
this.movementRepo = movementRepo;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Order create(OrderCreateRequest req) {
|
||||
if (req.items() == null || req.items().isEmpty()) {
|
||||
throw new IllegalArgumentException("Order must have at least 1 item");
|
||||
}
|
||||
|
||||
// 1) Stock check first (fail fast)
|
||||
for (var item : req.items()) {
|
||||
var sku = skuRepo.findById(item.skuId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unknown skuId: " + item.skuId()));
|
||||
|
||||
if (!sku.isActive()) {
|
||||
throw new IllegalArgumentException("SKU is inactive: " + sku.getId());
|
||||
}
|
||||
|
||||
int current = movementRepo.currentStock(sku.getId());
|
||||
if (item.qty() <= 0) throw new IllegalArgumentException("qty must be > 0");
|
||||
|
||||
if (current < item.qty()) {
|
||||
throw new IllegalArgumentException("Insufficient stock for skuId=" + sku.getId()
|
||||
+ " (current=" + current + ", requested=" + item.qty() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Create order + items
|
||||
Order order = new Order();
|
||||
order.setCustomerRef(req.customerRef());
|
||||
|
||||
for (var item : req.items()) {
|
||||
var sku = skuRepo.findById(item.skuId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unknown skuId: " + item.skuId()));
|
||||
|
||||
OrderItem oi = new OrderItem();
|
||||
oi.setSku(sku);
|
||||
oi.setQty(item.qty());
|
||||
oi.setUnitPrice(item.unitPrice());
|
||||
order.addItem(oi);
|
||||
}
|
||||
|
||||
Order saved = orderRepo.save(order);
|
||||
|
||||
// 3) Create SALE movements (negative)
|
||||
for (var oi : saved.getItems()) {
|
||||
InventoryMovement m = new InventoryMovement();
|
||||
m.setSku(oi.getSku());
|
||||
m.setDelta(-oi.getQty());
|
||||
m.setReason(MovementReason.SALE);
|
||||
m.setReference("ORDER:" + saved.getId());
|
||||
movementRepo.save(m);
|
||||
}
|
||||
|
||||
return saved;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user