Skip to main content

Shopping Cart Data Model

Overview

This document describes the data model for the Shopping Cart system in the Ink platform. The cart model is optimized for self-service ecommerce scenarios with real-time pricing, session persistence, and seamless checkout experiences.

Entity Relationship Diagram

Core Entities

Cart

The main shopping cart entity representing a customer's current shopping session.

// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/entity/Cart.java
@Entity
@Table(name = "carts", indexes = {
@Index(name = "idx_cart_user", columnList = "user_id"),
@Index(name = "idx_cart_session", columnList = "session_id"),
@Index(name = "idx_cart_status", columnList = "status"),
@Index(name = "idx_cart_expires_at", columnList = "expires_at")
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Cart {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user; // Null for guest carts

@Column(name = "session_id", length = 255)
private String sessionId; // Browser session ID for guest carts

@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 20)
private CartStatus status = CartStatus.ACTIVE;

@Column(name = "subtotal", precision = 19, scale = 2)
private BigDecimal subtotal = BigDecimal.ZERO;

@Column(name = "discount_total", precision = 19, scale = 2)
private BigDecimal discountTotal = BigDecimal.ZERO;

@Column(name = "shipping_total", precision = 19, scale = 2)
private BigDecimal shippingTotal = BigDecimal.ZERO;

@Column(name = "tax_total", precision = 19, scale = 2)
private BigDecimal taxTotal = BigDecimal.ZERO;

@Column(name = "total", precision = 19, scale = 2)
private BigDecimal total = BigDecimal.ZERO;

@Column(name = "currency", length = 3)
private String currency = "USD";

@Column(name = "expires_at")
private Instant expiresAt;

@Column(name = "abandoned_at")
private Instant abandonedAt;

@Column(name = "completed_at")
private Instant completedAt;

@Column(name = "ip_address", length = 45)
private String ipAddress;

@Column(name = "user_agent", length = 500)
private String userAgent;

@Column(name = "referrer", length = 500)
private String referrer;

@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true)
private List<CartItem> items = new ArrayList<>();

@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true)
private List<CartDiscount> discounts = new ArrayList<>();

@Column(name = "metadata", columnDefinition = "jsonb")
@JdbcTypeCode(SqlTypes.JSON)
private Map<String, Object> metadata = new HashMap<>();

@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;

@Column(name = "updated_at", nullable = false)
private Instant updatedAt;

@Version
private Integer version;

@PrePersist
protected void onCreate() {
createdAt = Instant.now();
updatedAt = Instant.now();
if (expiresAt == null) {
expiresAt = Instant.now().plus(48, ChronoUnit.HOURS); // Default 48 hour expiration
}
}

@PreUpdate
protected void onUpdate() {
updatedAt = Instant.now();
}

// Business logic methods

public void addItem(CartItem item) {
items.add(item);
item.setCart(this);
recalculateTotals();
}

public void removeItem(CartItem item) {
items.remove(item);
item.setCart(null);
recalculateTotals();
}

public void recalculateTotals() {
this.subtotal = items.stream()
.map(CartItem::getTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);

this.discountTotal = discounts.stream()
.map(CartDiscount::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);

// Tax and shipping calculated by external services
this.total = subtotal
.subtract(discountTotal)
.add(shippingTotal)
.add(taxTotal);
}

public boolean isExpired() {
return expiresAt != null && Instant.now().isAfter(expiresAt);
}

public boolean isAbandoned() {
return status == CartStatus.ABANDONED;
}

public boolean isActive() {
return status == CartStatus.ACTIVE && !isExpired();
}

public int getItemCount() {
return items.stream()
.mapToInt(CartItem::getQuantity)
.sum();
}
}

@Getter
public enum CartStatus {
ACTIVE("Active"),
CHECKOUT("Checkout In Progress"),
PAYMENT_PENDING("Payment Pending"),
PAYMENT_PROCESSING("Payment Processing"),
COMPLETED("Completed"),
ABANDONED("Abandoned"),
EXPIRED("Expired");

private final String displayName;

CartStatus(String displayName) {
this.displayName = displayName;
}
}

CartItem

Represents individual products added to the shopping cart.

// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/entity/CartItem.java
@Entity
@Table(name = "cart_items", indexes = {
@Index(name = "idx_cart_item_cart", columnList = "cart_id"),
@Index(name = "idx_cart_item_product", columnList = "product_id")
})
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CartItem {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "cart_id", nullable = false)
private Cart cart;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id", nullable = false)
private Product product;

@Column(name = "quantity", nullable = false)
private Integer quantity = 1;

@Column(name = "unit_price", nullable = false, precision = 19, scale = 2)
private BigDecimal unitPrice;

@Column(name = "discount_amount", precision = 19, scale = 2)
private BigDecimal discountAmount = BigDecimal.ZERO;

@Column(name = "total", precision = 19, scale = 2)
private BigDecimal total;

@OneToMany(mappedBy = "cartItem", cascade = CascadeType.ALL, orphanRemoval = true)
private List<CartItemDiscount> discounts = new ArrayList<>();

@Column(name = "metadata", columnDefinition = "jsonb")
@JdbcTypeCode(SqlTypes.JSON)
private Map<String, Object> metadata = new HashMap<>();

@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;

@Column(name = "updated_at", nullable = false)
private Instant updatedAt;

@PrePersist
protected void onCreate() {
createdAt = Instant.now();
updatedAt = Instant.now();
calculateTotal();
}

@PreUpdate
protected void onUpdate() {
updatedAt = Instant.now();
calculateTotal();
}

public void calculateTotal() {
BigDecimal itemTotal = unitPrice.multiply(new BigDecimal(quantity));
this.total = itemTotal.subtract(discountAmount);
}

public void updateQuantity(int newQuantity) {
if (newQuantity <= 0) {
throw new IllegalArgumentException("Quantity must be greater than 0");
}
this.quantity = newQuantity;
calculateTotal();
}
}

CartDiscount

Represents cart-level discounts (coupon codes, promotions).

// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/entity/CartDiscount.java
@Entity
@Table(name = "cart_discounts")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CartDiscount {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "cart_id", nullable = false)
private Cart cart;

@Enumerated(EnumType.STRING)
@Column(name = "discount_type", nullable = false, length = 50)
private DiscountType discountType;

@Column(name = "code", length = 50)
private String code; // Coupon code if applicable

@Column(name = "amount", precision = 19, scale = 2)
private BigDecimal amount;

@Column(name = "percentage", precision = 5, scale = 2)
private BigDecimal percentage;

@Column(name = "description", length = 500)
private String description;

@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;

@PrePersist
protected void onCreate() {
createdAt = Instant.now();
}
}

@Getter
public enum DiscountType {
COUPON_CODE("Coupon Code"),
PROMOTIONAL("Promotional Discount"),
VOLUME("Volume Discount"),
LOYALTY("Loyalty Reward"),
FIRST_PURCHASE("First Purchase Discount"),
SEASONAL("Seasonal Sale");

private final String displayName;

DiscountType(String displayName) {
this.displayName = displayName;
}
}

Data Transfer Objects (DTOs)

CartDto

// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/dto/cart/CartDto.java
@Data
@Builder
public class CartDto {
private Long id;
private Long userId;
private String sessionId;
private CartStatus status;
private List<CartItemDto> items;
private List<CartDiscountDto> discounts;
private BigDecimal subtotal;
private BigDecimal discountTotal;
private BigDecimal shippingTotal;
private BigDecimal taxTotal;
private BigDecimal total;
private String currency;
private int itemCount;
private Instant expiresAt;
private Instant createdAt;
private Instant updatedAt;
}

@Data
@Builder
public class CartItemDto {
private Long id;
private Long productId;
private String productName;
private String productSku;
private Integer quantity;
private BigDecimal unitPrice;
private BigDecimal discountAmount;
private BigDecimal total;
private String imageUrl;
}

@Data
@Builder
public class CartDiscountDto {
private Long id;
private DiscountType discountType;
private String code;
private BigDecimal amount;
private BigDecimal percentage;
private String description;
}

Request DTOs

// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/dto/cart/AddToCartRequest.java
@Data
@Builder
public class AddToCartRequest {
@NotNull
private Long productId;

@Min(1)
private Integer quantity = 1;

private Map<String, Object> metadata;
}

@Data
@Builder
public class UpdateCartItemRequest {
@Min(1)
private Integer quantity;
}

@Data
@Builder
public class ApplyCouponRequest {
@NotBlank
private String couponCode;
}

@Data
@Builder
public class CheckoutRequest {
@NotNull
private ShippingAddressDto shippingAddress;

@NotNull
private BillingAddressDto billingAddress;

@NotNull
private PaymentMethodDto paymentMethod;

private String notes;
}

Database Schema

-- filepath: /Users/jetstart/dev/jetrev/ink/src/main/resources/db/changelog/schema/cart-schema.sql

-- Carts table
CREATE TABLE carts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users(id),
session_id VARCHAR(255),
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
subtotal DECIMAL(19,2) DEFAULT 0,
discount_total DECIMAL(19,2) DEFAULT 0,
shipping_total DECIMAL(19,2) DEFAULT 0,
tax_total DECIMAL(19,2) DEFAULT 0,
total DECIMAL(19,2) DEFAULT 0,
currency VARCHAR(3) DEFAULT 'USD',
expires_at TIMESTAMP WITH TIME ZONE,
abandoned_at TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE,
ip_address VARCHAR(45),
user_agent VARCHAR(500),
referrer VARCHAR(500),
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
version INTEGER DEFAULT 0
);

CREATE INDEX idx_cart_user ON carts(user_id);
CREATE INDEX idx_cart_session ON carts(session_id);
CREATE INDEX idx_cart_status ON carts(status);
CREATE INDEX idx_cart_expires_at ON carts(expires_at);

-- Cart items table
CREATE TABLE cart_items (
id BIGSERIAL PRIMARY KEY,
cart_id BIGINT NOT NULL REFERENCES carts(id) ON DELETE CASCADE,
product_id BIGINT NOT NULL REFERENCES products(id),
quantity INTEGER NOT NULL DEFAULT 1,
unit_price DECIMAL(19,2) NOT NULL,
discount_amount DECIMAL(19,2) DEFAULT 0,
total DECIMAL(19,2) NOT NULL,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT cart_item_unique_product UNIQUE(cart_id, product_id)
);

CREATE INDEX idx_cart_item_cart ON cart_items(cart_id);
CREATE INDEX idx_cart_item_product ON cart_items(product_id);

-- Cart discounts table
CREATE TABLE cart_discounts (
id BIGSERIAL PRIMARY KEY,
cart_id BIGINT NOT NULL REFERENCES carts(id) ON DELETE CASCADE,
discount_type VARCHAR(50) NOT NULL,
code VARCHAR(50),
amount DECIMAL(19,2),
percentage DECIMAL(5,2),
description VARCHAR(500),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Cart item discounts table
CREATE TABLE cart_item_discounts (
id BIGSERIAL PRIMARY KEY,
cart_item_id BIGINT NOT NULL REFERENCES cart_items(id) ON DELETE CASCADE,
discount_type VARCHAR(50) NOT NULL,
amount DECIMAL(19,2) NOT NULL,
description VARCHAR(500),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Cart events for audit trail
CREATE TABLE cart_events (
id BIGSERIAL PRIMARY KEY,
cart_id BIGINT NOT NULL REFERENCES carts(id) ON DELETE CASCADE,
event_type VARCHAR(50) NOT NULL,
description VARCHAR(500),
data JSONB,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_cart_event_cart ON cart_events(cart_id);
CREATE INDEX idx_cart_event_type ON cart_events(event_type);

Next Steps: Implement Cart Service for business logic.