CustomerOrder.java
package com.rbctcsworld.ecommerce.order;
import com.rbctcsworld.ecommerce.common.exception.ConflictException;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OrderBy;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/** Named CustomerOrder because "Order" clashes with SQL ORDER BY and JPA's Order type. */
@Entity
@Table(name = "orders")
public class CustomerOrder {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "order_number", nullable = false, unique = true, length = 30)
private String orderNumber;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(nullable = false, length = 20)
private String status = OrderStatus.PLACED;
@Column(name = "total_items", nullable = false)
private int totalItems;
@Column(nullable = false, precision = 12, scale = 2)
private BigDecimal subtotal = BigDecimal.ZERO;
@Column(nullable = false, precision = 12, scale = 2)
private BigDecimal discount = BigDecimal.ZERO;
@Column(name = "shipping_fee", nullable = false, precision = 12, scale = 2)
private BigDecimal shippingFee = BigDecimal.ZERO;
@Column(nullable = false, precision = 12, scale = 2)
private BigDecimal tax = BigDecimal.ZERO;
@Column(nullable = false, precision = 12, scale = 2)
private BigDecimal total = BigDecimal.ZERO;
@Column(name = "coupon_code", length = 40)
private String couponCode;
@Column(name = "paid_at")
private LocalDateTime paidAt;
@Column(length = 10)
private String carrier;
@Column(name = "tracking_number", unique = true, length = 30)
private String trackingNumber;
@Column(name = "shipped_at")
private LocalDateTime shippedAt;
@Column(name = "delivered_at")
private LocalDateTime deliveredAt;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@Column(name = "cancelled_at")
private LocalDateTime cancelledAt;
// delivery address: a copy taken when the order is placed (V12); null for orders placed before
@Column(name = "ship_name", length = 80)
private String shipName;
@Column(name = "ship_line1", length = 100)
private String shipLine1;
@Column(name = "ship_line2", length = 100)
private String shipLine2;
@Column(name = "ship_city", length = 60)
private String shipCity;
@Column(name = "ship_state", length = 2)
private String shipState;
@Column(name = "ship_zip", length = 10)
private String shipZip;
@Column(name = "ship_phone", length = 20)
private String shipPhone;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("id ASC")
private List<OrderItem> items = new ArrayList<>();
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("id ASC")
private List<OrderEvent> events = new ArrayList<>();
protected CustomerOrder() {
}
public CustomerOrder(String orderNumber, Long userId) {
this.orderNumber = orderNumber;
this.userId = userId;
this.events.add(new OrderEvent(this, OrderStatus.PLACED, "Order placed"));
}
@PrePersist
void onCreate() {
createdAt = LocalDateTime.now();
}
public void addItem(OrderItem item) {
item.attachTo(this);
items.add(item);
totalItems += item.getQuantity();
subtotal = subtotal.add(item.getLineTotal());
}
/** Stores the final price breakdown (subtotal itself is accumulated by addItem). */
public void applyPricing(BigDecimal discount, BigDecimal shippingFee, BigDecimal tax, BigDecimal total,
String couponCode) {
this.discount = discount;
this.shippingFee = shippingFee;
this.tax = tax;
this.total = total;
this.couponCode = couponCode;
}
/** Stores where the order goes (expects a normalized address). */
public void shipTo(ShippingAddress a) {
this.shipName = a.fullName();
this.shipLine1 = a.line1();
this.shipLine2 = a.line2();
this.shipCity = a.city();
this.shipState = a.state();
this.shipZip = a.zip();
this.shipPhone = a.phone();
}
/** The delivery address, or null for an order placed before addresses existed. */
public ShippingAddress getShippingAddress() {
return shipName == null ? null
: new ShippingAddress(shipName, shipLine1, shipLine2, shipCity, shipState, shipZip, shipPhone);
}
// ------------------------------------------------------------ state machine
/** The ONLY way the status changes: checks OrderStatus.ALLOWED (409 otherwise) and records a timeline event. */
public void transitionTo(String target, String note) {
if (!OrderStatus.canMove(status, target)) {
throw new ConflictException("Order " + orderNumber + " cannot move from " + status + " to " + target);
}
status = target;
events.add(new OrderEvent(this, target, note));
}
public boolean isPaid() {
return OrderStatus.PAID.equals(status);
}
/** Throws 409 unless the order may currently move to {@code target} (used before side effects like charging). */
public void assertCanMoveTo(String target) {
if (!OrderStatus.canMove(status, target)) {
throw new ConflictException("Order " + orderNumber + " cannot move from " + status + " to " + target);
}
}
public void markPaid() {
transitionTo(OrderStatus.PAID, "Payment received");
paidAt = LocalDateTime.now();
}
public void ship(String carrier, String trackingNumber) {
transitionTo(OrderStatus.SHIPPED, "Shipped with " + carrier + ", tracking " + trackingNumber);
this.carrier = carrier;
this.trackingNumber = trackingNumber;
this.shippedAt = LocalDateTime.now();
}
public void deliver() {
transitionTo(OrderStatus.DELIVERED, "Delivered");
this.deliveredAt = LocalDateTime.now();
}
public void cancel() {
transitionTo(OrderStatus.CANCELLED, "Order cancelled");
cancelledAt = LocalDateTime.now();
}
public Long getId() { return id; }
public String getOrderNumber() { return orderNumber; }
public Long getUserId() { return userId; }
public String getStatus() { return status; }
public int getTotalItems() { return totalItems; }
public BigDecimal getSubtotal() { return subtotal; }
public BigDecimal getDiscount() { return discount; }
public BigDecimal getShippingFee() { return shippingFee; }
public BigDecimal getTax() { return tax; }
public BigDecimal getTotal() { return total; }
public String getCouponCode() { return couponCode; }
public LocalDateTime getPaidAt() { return paidAt; }
public String getCarrier() { return carrier; }
public String getTrackingNumber() { return trackingNumber; }
public LocalDateTime getShippedAt() { return shippedAt; }
public LocalDateTime getDeliveredAt() { return deliveredAt; }
public List<OrderEvent> getEvents() { return events; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getCancelledAt() { return cancelledAt; }
public List<OrderItem> getItems() { return items; }
}