Skip to content

Latest commit

 

History

History
955 lines (778 loc) · 29.4 KB

File metadata and controls

955 lines (778 loc) · 29.4 KB

Struct to Schema Migration Plan

Current Architecture Analysis

Current Structs

Auctioneer.Structs.Auction

defstruct [
  :id,                    # binary() - UUID
  :title,                 # String.t()
  :description,           # String.t() 
  :start_datetime,        # DateTime.t()
  :end_datetime,          # DateTime.t()
  :min_bid_fn,           # function() - PROBLEMATIC for DB
  :timer_ref,            # reference() - PROBLEMATIC for DB
  status: :planned,       # :planned | :active | :complete
  bids: :queue.from_list([%Bid{amount: Decimal.new(0)}])  # PROBLEMATIC for DB
]

Auctioneer.Structs.Bid

defstruct [
  :id,                    # binary() - UUID
  :amount,                # Decimal.t()
  :bidder,               # term() - needs clarification
  currency: "USD"         # String.t()
]

Current Server Types Analysis

  1. PureGenServer - Simple English auction with bid validation
  2. EtsServer - English auction with ETS caching for performance
  3. VickreyServer - Sealed bid auction, winner pays second-highest
  4. DutchServer - Descending price auction with timer-based decreases

Key Issues for Database Migration

Problematic Fields in Current Structs

  1. min_bid_fn (function): Can be stored as binary blob using term_to_binary/1

    • Current Usage: Dynamic bid validation logic
    • Database Storage: Use :binary field with term_to_binary/1 and binary_to_term/1
    • Examples:
      • English: latest_bid + increment
      • Dutch: current_price - decrement
      • Vickrey: reserve_price
  2. timer_ref (reference): Process-specific, cannot be persisted

    • Current Usage: Only in DutchServer for price decreases
    • Alternative: Store timer interval and manage in process
  3. bids (queue): Queue data structure not DB-compatible

    • Current Usage: In-memory bid collection
    • Alternative: Separate bids table with foreign key relationship
  4. bidder (term): Too generic for proper DB constraints

    • Current Usage: String identifiers in tests
    • Alternative: Proper user references via Phoenix authentication (mix phx.gen.auth)

Migration Strategy

Phase 1: Schema Design

Auctions Table

mix phx.gen.live Auctions Auction auctions \
  title:string \
  description:text \
  auction_type:enum:english:dutch:vickrey \
  start_datetime:utc_datetime \
  end_datetime:utc_datetime \
  status:enum:planned:active:completed:cancelled \
  starting_price:decimal \
  reserve_price:decimal \
  bid_increment:decimal \
  price_decrement:decimal \
  decrease_interval:integer \
  min_bid_fn:binary \
  winner_id:references:users \
  winning_amount:decimal \
  --binary-id

Bids Table

mix phx.gen.live Bids Bid bids \
  auction_id:references:auctions \
  user_id:references:users \
  amount:decimal \
  currency:string \
  --binary-id

Users/Authentication (Phoenix Built-in Auth)

mix phx.gen.auth Accounts User users --binary-id

Phase 2: Architectural Decisions Based on Research

Key Research Findings

1. Ecto Changesets and Dirty Tracking:

  • Ecto changesets provide built-in change tracking and validation
  • Phoenix LiveView integrates seamlessly with changesets for real-time validation
  • No need for custom "dirty" flags - Ecto handles this automatically
  • Changesets only track changes when using proper Ecto operations

2. In-Memory vs Database State Management:

  • LiveView processes are stateful on the server side via WebSocket connections
  • Best practice: Use in-memory state for temporary, session-specific data
  • Use database for data that needs to survive server restarts
  • Hybrid approach works well: process state for active operations, DB for persistence

3. Bid Type Enum Removal:

  • Research shows enums can be unnecessary complexity for simple use cases
  • User associations provide better data integrity than enum flags
  • System users can be regular users in the user table with special IDs
  • Simpler schema design reduces maintenance overhead

4. Ecto Associations for Bid Management:

  • Ecto provides belongs_to/has_many for auction-bid relationships
  • cast_assoc/3 and put_assoc/3 handle complex nested updates
  • Preloading strategies: :preload option in queries, Repo.preload/2 for existing records
  • Association configuration controls update/delete behavior with :on_delete options
  • Use Ecto.build_assoc/3 for creating child records with proper parent association
  • Prefer query-based operations over loading entire datasets for performance

Phase 2: Function Serialization Strategy

Storing min_bid_fn as Binary Blob

Database Storage:

  • Use :binary field to store serialized function
  • Serialize with term_to_binary/1 when saving to DB
  • Deserialize with binary_to_term/1 when loading from DB

Security Considerations:

  • Only deserialize trusted function blobs
  • Consider function validation/whitelisting
  • Use :safe option with binary_to_term/2 if needed

Backward Compatibility:

  • Keep helper fields (bid_increment, price_decrement, etc.) for querying
  • Use function as primary logic, fields as metadata
  • Enables complex custom bidding logic while maintaining DB queries

Database Schema Updates

# lib/auctioneer/auctions/auction.ex
defmodule Auctioneer.Auctions.Auction do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  
  schema "auctions" do
    field :title, :string
    field :description, :string
    field :auction_type, Ecto.Enum, values: [:english, :dutch, :vickrey]
    field :status, Ecto.Enum, values: [:planned, :active, :completed, :cancelled]
    
    # Timing
    field :start_datetime, :utc_datetime
    field :end_datetime, :utc_datetime
    
    # Pricing
    field :starting_price, :decimal
    field :reserve_price, :decimal
    field :bid_increment, :decimal      # English auctions (metadata)
    field :price_decrement, :decimal    # Dutch auctions (metadata) 
    field :decrease_interval, :integer  # Dutch auctions (ms)
    field :min_bid_fn, :binary          # Serialized function
    
    # Results
    belongs_to :winner, Auctioneer.Accounts.User
    field :winning_amount, :decimal
    
    # Relationships
    has_many :bids, Auctioneer.Bids.Bid
    
    timestamps(type: :utc_datetime)
  end
  
  def changeset(auction, attrs) do
    auction
    |> cast(attrs, [:title, :description, :auction_type, :status, 
                    :start_datetime, :end_datetime, :starting_price, 
                    :reserve_price, :bid_increment, :price_decrement, 
                    :decrease_interval, :min_bid_fn, :winner_id, :winning_amount])
    |> validate_required([:title, :auction_type, :status, :starting_price])
    |> validate_inclusion(:auction_type, [:english, :dutch, :vickrey])
    |> validate_inclusion(:status, [:planned, :active, :completed, :cancelled])
    |> validate_number(:starting_price, greater_than: 0)
    |> put_min_bid_fn_if_needed()
    |> validate_auction_type_fields()
  end
  
  defp validate_auction_type_fields(changeset) do
    case get_field(changeset, :auction_type) do
      :english ->
        changeset
        |> validate_required([:bid_increment])
        |> validate_number(:bid_increment, greater_than: 0)
        
      :dutch ->
        changeset
        |> validate_required([:price_decrement, :decrease_interval])
        |> validate_number(:price_decrement, greater_than: 0)
        |> validate_number(:decrease_interval, greater_than: 1000)
        
      :vickrey ->
        changeset
        |> validate_required([:reserve_price])
        |> validate_number(:reserve_price, greater_than_or_equal_to: 0)
        
      _ ->
        changeset
    end
  end
  
  defp put_min_bid_fn_if_needed(changeset) do
    case {get_field(changeset, :min_bid_fn), get_field(changeset, :auction_type)} do
      {nil, auction_type} when auction_type != nil ->
        function = default_min_bid_fn(auction_type, changeset)
        put_change(changeset, :min_bid_fn, :erlang.term_to_binary(function))
      
      _ ->
        changeset
    end
  end
  
  defp default_min_bid_fn(:english, changeset) do
    bid_increment = get_field(changeset, :bid_increment) || Decimal.new("1.00")
    
    fn auction ->
      case :queue.peek_r(auction.bids) do
        {:value, latest_bid} -> Decimal.add(latest_bid.amount, bid_increment)
        :empty -> get_field(changeset, :starting_price) || Decimal.new("0.00")
      end
    end
  end
  
  defp default_min_bid_fn(:dutch, changeset) do
    price_decrement = get_field(changeset, :price_decrement) || Decimal.new("5.00")
    
    fn auction ->
      case :queue.peek_r(auction.bids) do
        {:value, latest_bid} -> Decimal.sub(latest_bid.amount, price_decrement)
        :empty -> get_field(changeset, :starting_price) || Decimal.new("100.00")
      end
    end
  end
  
  defp default_min_bid_fn(:vickrey, changeset) do
    reserve_price = get_field(changeset, :reserve_price) || Decimal.new("0.00")
    
    fn _auction -> reserve_price end
  end
  
  # Helper to deserialize function from database
  def get_min_bid_fn(%__MODULE__{min_bid_fn: nil}), do: nil
  def get_min_bid_fn(%__MODULE__{min_bid_fn: binary_fn}) when is_binary(binary_fn) do
    :erlang.binary_to_term(binary_fn)
  end
end
# lib/auctioneer/bids/bid.ex
defmodule Auctioneer.Bids.Bid do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id

  schema "bids" do
    field :amount, :decimal
    field :currency, :string, default: "USD"
    
    belongs_to :auction, Auctioneer.Auctions.Auction
    belongs_to :user, Auctioneer.Accounts.User
    
    timestamps(type: :utc_datetime)
  end

  def changeset(bid, attrs) do
    bid
    |> cast(attrs, [:amount, :currency, :auction_id, :user_id])
    |> validate_required([:amount, :auction_id, :user_id])
    |> validate_number(:amount, greater_than: 0)
    |> validate_inclusion(:currency, ["USD", "EUR", "GBP", "CAD"])
    |> foreign_key_constraint(:auction_id)
    |> foreign_key_constraint(:user_id)
  end
end

Phase 3: Server Architecture Updates

Simplified Architecture: In-Memory During Auction, DB for Persistence

Process State (keep in GenServer):

  • Complete %Auction{} struct with embedded bids queue
  • timer_ref: Timer references for Dutch auctions only
  • No separate bid cache - use auction.bids queue directly

Database State (persist to DB):

  • Auction metadata and configuration (on start)
  • Final auction results and winner (on completion)
  • Individual bids written to DB only when auction completes

Updated Server Pattern

defmodule Auctioneer.Servers.SimplifiedAuctionServer do
  use GenServer
  alias Auctioneer.{Auctions, Bids}
  alias Auctioneer.Structs.{Auction, Bid}
  
  # State is simply the auction struct itself with timer_ref field
  
  def start_link(%Auction{} = auction) do
    GenServer.start_link(__MODULE__, auction, name: via_tuple(auction.id))
  end
  
  def init(%Auction{} = auction) do
    # Load min_bid_fn from binary field
    min_bid_fn = Auctions.Auction.get_min_bid_fn(auction)
    auction = %{auction | min_bid_fn: min_bid_fn}
    
    timer_ref = maybe_start_timer(auction)
    auction = %{auction | timer_ref: timer_ref}
    
    Phoenix.PubSub.broadcast(
      Auctioneer.PubSub,
      "auctions:#{auction.id}",
      {:auction_started, auction}
    )
    
    {:ok, auction}
  end
  
  def handle_cast({:place_bid, bid_params}, auction) do
    bid = %Bid{
      id: Ecto.UUID.generate(),
      amount: bid_params.amount,
      bidder: bid_params.user_id,  # Will be resolved via association later
      currency: bid_params.currency || "USD"
    }
    
    case validate_bid(auction, bid) do
      :ok ->
        updated_auction = %{auction | bids: :queue.in(bid, auction.bids)}
        
        Phoenix.PubSub.broadcast(
          Auctioneer.PubSub,
          "auctions:#{auction.id}",
          {:bid_placed, updated_auction}
        )
        
        {:noreply, updated_auction}
      
      {:error, _reason} ->
        {:noreply, auction}
    end
  end
  
  # Dutch auction price decrease
  def handle_info(:decrease_price, auction) do
    case auction.auction_type do
      :dutch ->
        new_price = auction.min_bid_fn.(auction)
        
        system_bid = %Bid{
          id: Ecto.UUID.generate(),
          amount: new_price,
          bidder: get_system_user_id(),
          currency: "USD"
        }
        
        updated_auction = %{auction | bids: :queue.in(system_bid, auction.bids)}
        
        Phoenix.PubSub.broadcast(
          Auctioneer.PubSub,
          "auctions:#{auction.id}",
          {:price_decreased, updated_auction}
        )
        
        timer_ref = Process.send_after(self(), :decrease_price, auction.decrease_interval)
        updated_auction = %{updated_auction | timer_ref: timer_ref}
        
        {:noreply, updated_auction}
        
      _ ->
        {:noreply, auction}
    end
  end
  
  defp validate_bid(auction, bid) do
    min_amount = auction.min_bid_fn.(auction)
    
    if Decimal.compare(bid.amount, min_amount) == :lt do
      {:error, :bid_too_low}
    else
      :ok
    end
  end
  
  # When auction ends, persist all bids to database
  def handle_call(:end_auction, _from, auction) do
    case determine_winner(auction) do
      {:ok, {winner_id, winning_amount}} ->
        # Update auction in database with winner
        {:ok, auction_record} = Auctions.complete_auction(auction, winner_id, winning_amount)
        
        # Persist all bids to database using proper associations
        persist_auction_bids(auction_record, auction.bids)
        
        Phoenix.PubSub.broadcast(
          Auctioneer.PubSub,
          "auctions:#{auction.id}",
          {:auction_ended, auction}
        )
        
        {:stop, :normal, :ok, auction}
      
      {:error, reason} ->
        {:reply, {:error, reason}, auction}
    end
  end
  
  defp persist_auction_bids(auction_record, bids_queue) do
    # Convert queue to list and insert using bulk operation for performance
    bid_list = :queue.to_list(bids_queue)
    
    # Use Repo.insert_all for better performance with many bids
    bid_data = Enum.map(bid_list, fn bid ->
      %{
        id: Ecto.UUID.generate(),
        auction_id: auction_record.id,
        user_id: bid.bidder,
        amount: bid.amount,
        currency: bid.currency,
        inserted_at: DateTime.utc_now(),
        updated_at: DateTime.utc_now()
      }
    end)
    
    Repo.insert_all(Bids.Bid, bid_data, on_conflict: :nothing)
  end
end

Phase 4: Migration Steps

Step 1: Generate Authentication and LiveView Resources

# Generate Phoenix built-in authentication first
mix phx.gen.auth Accounts User users --binary-id

# Generate Auctions LiveView
mix phx.gen.live Auctions Auction auctions \
  title:string \
  description:text \
  auction_type:enum:english:dutch:vickrey \
  start_datetime:utc_datetime \
  end_datetime:utc_datetime \
  status:enum:planned:active:completed:cancelled \
  starting_price:decimal \
  reserve_price:decimal \
  bid_increment:decimal \
  price_decrement:decimal \
  decrease_interval:integer \
  min_bid_fn:binary \
  --binary-id

# Generate Bids LiveView  
mix phx.gen.live Bids Bid bids \
  auction_id:references:auctions \
  user_id:references:users \
  amount:decimal \
  currency:string \
  --binary-id

Step 2: Update Database Constraints

Add additional migration after generation:

defmodule Auctioneer.Repo.Migrations.AddAuctionConstraints do
  use Ecto.Migration

  def change do
    # Add winner foreign key to auctions  
    alter table(:auctions) do
      add :winner_id, references(:users, type: :binary_id, on_delete: :nilify_all)
      add :winning_amount, :decimal, precision: 15, scale: 2
    end
    
    # Ensure min_bid_fn is added if not generated automatically
    alter table(:auctions) do
      modify :min_bid_fn, :binary, null: false
    end

    # Add indexes for performance
    create index(:auctions, [:status])
    create index(:auctions, [:auction_type])
    create index(:auctions, [:start_datetime])
    create index(:bids, [:auction_id, :inserted_at])
    create index(:bids, [:user_id])
    
    # Add check constraints
    create constraint(:auctions, :positive_starting_price, check: "starting_price > 0")
    create constraint(:auctions, :positive_bid_increment, check: "bid_increment > 0")
    create constraint(:auctions, :positive_price_decrement, check: "price_decrement > 0")
    create constraint(:auctions, :valid_decrease_interval, check: "decrease_interval > 1000")
    create constraint(:bids, :positive_amount, check: "amount > 0")
    
    # Add system user for system-generated bids (Dutch auctions)
    execute """
    INSERT INTO users (id, email, hashed_password, confirmed_at, inserted_at, updated_at)
    VALUES (
      '00000000-0000-0000-0000-000000000000',
      'system@auctioneer.local',
      '$2b$12$placeholder_hash_for_system_user_no_login',
      NOW(),
      NOW(),
      NOW()
    );
    """, ""
  end
end

Step 3: Update Context Functions

# lib/auctioneer/auctions.ex
defmodule Auctioneer.Auctions do
  import Ecto.Query, warn: false
  alias Auctioneer.Repo
  alias Auctioneer.Auctions.Auction

  def get_auction!(id), do: Repo.get!(Auction, id)
  
  def get_auction_with_bids!(id) do
    Auction
    |> Repo.get!(id)
    |> Repo.preload([bids: [:user], winner: []])
  end
  
  def list_active_auctions do
    Auction
    |> where([a], a.status == :active)
    |> order_by([a], asc: a.start_datetime)
    |> Repo.all()
  end
  
  def create_auction(attrs \\ %{}) do
    %Auction{}
    |> Auction.changeset(attrs)
    |> Repo.insert()
  end
  
  def start_auction(%Auction{} = auction) do
    auction
    |> Auction.changeset(%{status: :active})
    |> Repo.update()
    |> case do
      {:ok, updated_auction} ->
        # Start the GenServer process
        case Auctioneer.start_auction(updated_auction, auction_server_type(auction)) do
          {:ok, _pid} -> {:ok, updated_auction}
          error -> error
        end
      error -> error
    end
  end
  
  def complete_auction(%Auction{} = auction, winner_id, winning_amount) do
    auction
    |> Auction.changeset(%{
      status: :completed, 
      winner_id: winner_id, 
      winning_amount: winning_amount
    })
    |> Repo.update()
  end
  
  defp auction_server_type(%Auction{auction_type: :english}), do: :ets_server
  defp auction_server_type(%Auction{auction_type: :dutch}), do: :dutch_server  
  defp auction_server_type(%Auction{auction_type: :vickrey}), do: :vickrey_server
end

# lib/auctioneer/bids.ex
defmodule Auctioneer.Bids do
  import Ecto.Query, warn: false
  alias Auctioneer.Repo
  alias Auctioneer.Bids.Bid
  alias Auctioneer.Auctions.Auction

  def get_bid!(id), do: Repo.get!(Bid, id)
  
  def list_auction_bids(auction_id) do
    Bid
    |> where([b], b.auction_id == ^auction_id)
    |> order_by([b], desc: b.inserted_at)
    |> preload([:user])
    |> Repo.all()
  end
  
  def create_bid(attrs \\ %{}) do
    %Bid{}
    |> Bid.changeset(attrs)
    |> Repo.insert()
  end
  
  def create_bid_for_auction(auction_id, attrs) do
    # Use proper association handling
    case Repo.get(Auction, auction_id) do
      nil -> {:error, :auction_not_found}
      auction ->
        Ecto.build_assoc(auction, :bids)
        |> Bid.changeset(attrs)
        |> Repo.insert()
    end
  end
  
  def change_bid(%Bid{} = bid, attrs \\ %{}) do
    Bid.changeset(bid, attrs)
  end
end

Step 4: Update Server Modules

  1. Remove DETS persistence from all servers
  2. Add DB context functions for auction/bid operations
  3. Update min_bid logic to use schema fields instead of functions
  4. Maintain backwards compatibility during transition

Example update for VickreyServer:

defmodule Auctioneer.Servers.VickreyServer do
  use GenServer
  alias Auctioneer.{Auctions, Bids}
  
  def init(auction_id) do
    auction = Auctions.get_auction!(auction_id)
    
    Phoenix.PubSub.broadcast(
      Auctioneer.PubSub,
      "auctions:#{auction.id}",
      {:auction_started, auction}
    )

    {:ok, %{auction: auction, bid_cache: []}}
  end
  
  def handle_cast({:place_bid, bid_params}, auction) do
    bid = %Bid{
      id: Ecto.UUID.generate(),
      amount: bid_params.amount,
      bidder: bid_params.user_id,
      currency: bid_params.currency || "USD"
    }
    
    case validate_bid(auction, bid) do
      :ok ->
        updated_auction = %{auction | bids: :queue.in(bid, auction.bids)}
        
        Phoenix.PubSub.broadcast(
          Auctioneer.PubSub,
          "auctions:#{auction.id}",
          {:bid_placed, updated_auction}
        )
        
        {:noreply, updated_auction}
      
      {:error, _reason} ->
        {:noreply, auction}
    end
  end
  
  def handle_call(:auction_winner, _from, auction) do
    case determine_vickrey_winner(auction.bids) do
      {:ok, {winner_id, amount}} ->
        {:reply, {:ok, {winner_id, amount}}, auction}
      
      error ->
        {:reply, error, auction}
    end
  end
  
  # Uses in-memory bid queue for winner determination
end

Step 5: Update Tests

Replace struct-based test helpers with schema factories:

# test/support/fixtures/auctions_fixtures.ex
defmodule Auctioneer.AuctionsFixtures do
  def auction_fixture(attrs \\ %{}) do
    {:ok, auction} =
      attrs
      |> Enum.into(%{
        title: "Test Auction",
        description: "A test auction",
        auction_type: :english,
        status: :planned,
        starting_price: Decimal.new("10.00"),
        bid_increment: Decimal.new("1.00"),
        start_datetime: DateTime.utc_now(),
        end_datetime: DateTime.add(DateTime.utc_now(), 3600)
        # min_bid_fn will be auto-generated based on auction_type
      })
      |> Auctioneer.Auctions.create_auction()
      
    auction
  end
  
  def dutch_auction_fixture(attrs \\ %{}) do
    attrs
    |> Enum.into(%{
      auction_type: :dutch,
      starting_price: Decimal.new("100.00"),
      price_decrement: Decimal.new("5.00"),
      decrease_interval: 10_000
    })
    |> auction_fixture()
  end
  
  def vickrey_auction_fixture(attrs \\ %{}) do
    attrs
    |> Enum.into(%{
      auction_type: :vickrey,
      reserve_price: Decimal.new("5.00")
    })
    |> auction_fixture()
  end
end

# test/support/fixtures/bids_fixtures.ex  
defmodule Auctioneer.BidsFixtures do
  def bid_fixture(auction, user, attrs \\ %{}) do
    {:ok, bid} =
      attrs
      |> Enum.into(%{
        amount: Decimal.new("15.00"),
        currency: "USD",
        auction_id: auction.id,
        user_id: user.id
      })
      |> Auctioneer.Bids.create_bid()
      
    bid
  end
end

Step 6: LiveView Features

Add real-time auction interfaces:

# lib/auctioneer_web/live/auction_live/show.ex
defmodule AuctioneerWeb.AuctionLive.Show do
  use AuctioneerWeb, :live_view
  alias Auctioneer.{Auctions, Bids}

  def mount(_params, _session, socket) do
    if connected?(socket) do
      Phoenix.PubSub.subscribe(Auctioneer.PubSub, "auctions:#{socket.assigns.auction.id}")
    end
    
    {:ok, socket}
  end

  def handle_params(%{"id" => id}, _, socket) do
    auction = Auctions.get_auction_with_bids!(id)
    
    {:noreply,
     socket
     |> assign(:auction, auction)
     |> assign(:bid_form, to_form(Bids.change_bid(%Bids.Bid{})))}
  end

  def handle_event("place_bid", %{"bid" => bid_params}, socket) do
    auction_id = socket.assigns.auction.id
    bid_params_with_auction = Map.put(bid_params, "auction_id", auction_id)
    
    case Bids.create_bid_for_auction(auction_id, bid_params_with_auction) do
      {:ok, _bid} ->
        {:noreply, socket}
      
      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, :bid_form, to_form(changeset))}
    end
  end

  def handle_info({:bid_placed, updated_auction}, socket) do
    # Reload auction with latest bids from database
    auction_with_bids = Auctions.get_auction_with_bids!(socket.assigns.auction.id)
    {:noreply, assign(socket, :auction, auction_with_bids)}
  end

  def handle_info({:price_decreased, updated_auction}, socket) do
    # Handle Dutch auction price updates - reload from database
    auction_with_bids = Auctions.get_auction_with_bids!(socket.assigns.auction.id)
    {:noreply, assign(socket, :auction, auction_with_bids)}
  end

  def render(assigns) do
    ~H"""
    <div class="auction-container">
      <h1><%= @auction.title %></h1>
      <p><%= @auction.description %></p>
      
      <div class="auction-info">
        <span>Type: <%= @auction.auction_type %></span>
        <span>Status: <%= @auction.status %></span>
        <span>Current Price: $<%= current_price(@auction) %></span>
      </div>

      <div class="bids-section">
        <h3>Recent Bids</h3>
        <div class="bids-list">
          <%= for bid <- Enum.take(@auction.bids, 10) do %>
            <div class="bid-item">
              $<%= bid.amount %> - <%= if bid.user, do: bid.user.email, else: "System" %> 
              <span class="timestamp"><%= bid.inserted_at %></span>
            </div>
          <% end %>
        </div>
      </div>

      <%= if @auction.status == :active do %>
        <div class="bid-form">
          <.simple_form for={@bid_form} phx-submit="place_bid">
            <.input field={@bid_form[:amount]} type="number" label="Bid Amount" step="0.01" />
            <:actions>
              <.button>Place Bid</.button>
            </:actions>
          </.simple_form>
        </div>
      <% end %>
    </div>
    """
  end

  defp current_price(auction) do
    case List.first(auction.bids) do
      nil -> auction.starting_price
      bid -> bid.amount
    end
  end
end

Phase 5: Benefits of New Architecture

Advantages

  1. Persistent Storage: No data loss on server restarts
  2. Web Interface: Full CRUD operations via LiveView
  3. Scalability: Database queries vs in-memory limitations
  4. Audit Trail: Complete bid history with timestamps
  5. User Management: Proper user accounts and bidder tracking
  6. Reporting: SQL-based analytics and reporting
  7. Backup/Recovery: Standard database backup procedures
  8. Multi-tenancy: Easy to add organization/tenant isolation
  9. API Ready: Generated contexts work well for future API endpoints
  10. Custom Logic: Function serialization enables complex custom bidding rules
  11. Flexibility: Can store any Elixir function for min bid calculation

Preserved Features

  1. Real-time Performance: Cached bid data for fast operations
  2. Auction Types: All existing auction mechanics preserved
  3. PubSub Events: Live updates continue to work
  4. Process Supervision: OTP supervision tree unchanged
  5. Test Coverage: All existing tests adapted to new architecture

Phase 6: Cleanup Tasks

Remove DETS Dependencies

  1. Delete DETS functions from servers
  2. Remove DETS setup from application.ex
  3. Update tests to not expect DETS persistence
  4. Clean up artifacts like priv/completed_auctions.dets

Documentation Updates

  1. Update README with new setup instructions
  2. Add LiveView routes documentation
  3. Document schema relationships and constraints
  4. Update API examples to use contexts instead of structs

Recommended Implementation Order

  1. Generate Phoenix authentication (mix phx.gen.auth Accounts User users --binary-id) ✅
  2. Generate auction and bid schemas with LiveView interfaces ✅
  3. Add database constraints and relationships ✅
  4. Update one server type (start with PureGenServer - simplest) ✅
  5. Migrate tests for that server type ✅
  6. Add basic LiveView interface for auction management ✅
  7. Repeat for other server types (EtsServer, VickreyServer, DutchServer) ✅
  8. Add real-time bidding LiveView features with user authentication ✅
  9. Remove DETS code and update documentation ✅
  10. Performance testing and optimization ✅

This phased approach ensures minimal disruption while gaining the benefits of persistent storage, a modern web interface, and maintainable code that follows Phoenix/Ecto conventions.

Association-Based Improvements

Based on the Ecto associations documentation review, the following key improvements have been made to the migration plan:

Enhanced Relationship Management

  1. Proper Association Setup:

    • Auction has_many :bids with proper foreign key constraints
    • Bid belongs_to both :auction and :user for complete relationships
    • User can have_many :bids for bidding history
  2. Optimized Preloading Strategies:

    • get_auction_with_bids!/1 preloads nested associations: [bids: [:user], winner: []]
    • list_auction_bids/1 includes user preloading for efficient queries
    • Eliminates N+1 query problems in LiveView interfaces
  3. Association-Aware Operations:

    • create_bid_for_auction/2 uses Ecto.build_assoc/3 for proper parent-child relationships
    • Bulk bid persistence uses Repo.insert_all/3 with proper foreign keys
    • LiveView updates reload complete associations rather than manual state manipulation
  4. Performance Optimizations:

    • Batch operations for multiple bid inserts during auction completion
    • Query-based updates over in-memory manipulations where appropriate
    • Proper indexing on foreign key relationships for fast lookups
  5. Data Integrity:

    • Foreign key constraints prevent orphaned records
    • :on_delete options control cascade behavior
    • Association validation through changesets

These improvements ensure the migration follows Ecto best practices for relationship management, providing better performance, data integrity, and maintainability while preserving the real-time auction functionality.