hensei-api/docs/implementation/collection-tracking-implementation.md

30 KiB

Collection Tracking Implementation Guide

Prerequisites

  • Rails 8.0.1 environment set up
  • PostgreSQL database running
  • Basic understanding of the existing codebase structure
  • Familiarity with Rails migrations, models, and controllers

Step-by-Step Implementation

Step 1: Create Database Migrations

1.0 Add collection privacy levels to Users table

rails generate migration AddCollectionPrivacyToUsers
# db/migrate/xxx_add_collection_privacy_to_users.rb
class AddCollectionPrivacyToUsers < ActiveRecord::Migration[8.0]
  def change
    # Privacy levels: 0 = public, 1 = crew_only, 2 = private
    add_column :users, :collection_privacy, :integer, default: 0, null: false
    add_index :users, :collection_privacy
  end
end

1.1 Create CollectionCharacters migration

rails generate migration CreateCollectionCharacters
# db/migrate/xxx_create_collection_characters.rb
class CreateCollectionCharacters < ActiveRecord::Migration[8.0]
  def change
    create_table :collection_characters, id: :uuid do |t|
      t.references :user, type: :uuid, null: false, foreign_key: true
      t.references :character, type: :uuid, null: false, foreign_key: true
      t.integer :uncap_level, default: 0, null: false
      t.integer :transcendence_step, default: 0, null: false
      t.boolean :perpetuity, default: false, null: false
      t.references :awakening, type: :uuid, foreign_key: true
      t.integer :awakening_level, default: 1

      t.jsonb :ring1, default: { modifier: nil, strength: nil }, null: false
      t.jsonb :ring2, default: { modifier: nil, strength: nil }, null: false
      t.jsonb :ring3, default: { modifier: nil, strength: nil }, null: false
      t.jsonb :ring4, default: { modifier: nil, strength: nil }, null: false
      t.jsonb :earring, default: { modifier: nil, strength: nil }, null: false

      t.timestamps
    end

    add_index :collection_characters, [:user_id, :character_id], unique: true
    add_index :collection_characters, :user_id
    add_index :collection_characters, :character_id
  end
end

1.2 Create CollectionWeapons migration

rails generate migration CreateCollectionWeapons
# db/migrate/xxx_create_collection_weapons.rb
class CreateCollectionWeapons < ActiveRecord::Migration[8.0]
  def change
    create_table :collection_weapons, id: :uuid do |t|
      t.references :user, type: :uuid, null: false, foreign_key: true
      t.references :weapon, type: :uuid, null: false, foreign_key: true
      t.integer :uncap_level, default: 0, null: false
      t.integer :transcendence_step, default: 0

      t.references :weapon_key1, type: :uuid, foreign_key: { to_table: :weapon_keys }
      t.references :weapon_key2, type: :uuid, foreign_key: { to_table: :weapon_keys }
      t.references :weapon_key3, type: :uuid, foreign_key: { to_table: :weapon_keys }
      t.string :weapon_key4_id

      t.references :awakening, type: :uuid, foreign_key: true
      t.integer :awakening_level, default: 1, null: false

      t.integer :ax_modifier1
      t.float :ax_strength1
      t.integer :ax_modifier2
      t.float :ax_strength2
      t.integer :element

      t.timestamps
    end

    add_index :collection_weapons, :user_id
    add_index :collection_weapons, :weapon_id
    add_index :collection_weapons, [:user_id, :weapon_id]
  end
end

1.3 Create CollectionSummons migration

rails generate migration CreateCollectionSummons
# db/migrate/xxx_create_collection_summons.rb
class CreateCollectionSummons < ActiveRecord::Migration[8.0]
  def change
    create_table :collection_summons, id: :uuid do |t|
      t.references :user, type: :uuid, null: false, foreign_key: true
      t.references :summon, type: :uuid, null: false, foreign_key: true
      t.integer :uncap_level, default: 0, null: false
      t.integer :transcendence_step, default: 0, null: false

      t.timestamps
    end

    add_index :collection_summons, :user_id
    add_index :collection_summons, :summon_id
    add_index :collection_summons, [:user_id, :summon_id]
  end
end

1.4 Create CollectionJobAccessories migration

rails generate migration CreateCollectionJobAccessories
# db/migrate/xxx_create_collection_job_accessories.rb
class CreateCollectionJobAccessories < ActiveRecord::Migration[8.0]
  def change
    create_table :collection_job_accessories, id: :uuid do |t|
      t.references :user, type: :uuid, null: false, foreign_key: true
      t.references :job_accessory, type: :uuid, null: false, foreign_key: true

      t.timestamps
    end

    add_index :collection_job_accessories, [:user_id, :job_accessory_id],
              unique: true, name: 'idx_collection_job_acc_user_accessory'
    add_index :collection_job_accessories, :user_id
    add_index :collection_job_accessories, :job_accessory_id
  end
end

1.5 Run migrations

rails db:migrate

Step 2: Create Models

2.1 Create CollectionCharacter model

# app/models/collection_character.rb
class CollectionCharacter < ApplicationRecord
  belongs_to :user
  belongs_to :character
  belongs_to :awakening, optional: true

  validates :character_id, uniqueness: { scope: :user_id,
    message: "already exists in your collection" }
  validates :uncap_level, inclusion: { in: 0..5 }
  validates :transcendence_step, inclusion: { in: 0..10 }
  validates :awakening_level, inclusion: { in: 1..10 }

  validate :validate_rings
  validate :validate_awakening_compatibility

  scope :by_element, ->(element) { joins(:character).where(characters: { element: element }) }
  scope :by_rarity, ->(rarity) { joins(:character).where(characters: { rarity: rarity }) }
  scope :transcended, -> { where('transcendence_step > 0') }
  scope :with_awakening, -> { where.not(awakening_id: nil) }

  def blueprint
    CollectionCharacterBlueprint
  end

  private

  def validate_rings
    [ring1, ring2, ring3, ring4, earring].each_with_index do |ring, index|
      next unless ring['modifier'].present? || ring['strength'].present?

      if ring['modifier'].blank? || ring['strength'].blank?
        errors.add(:base, "Ring #{index + 1} must have both modifier and strength")
      end
    end
  end

  def validate_awakening_compatibility
    return unless awakening.present?

    unless awakening.object_type == 'Character'
      errors.add(:awakening, "must be a character awakening")
    end
  end
end

2.2 Create CollectionWeapon model

# app/models/collection_weapon.rb
class CollectionWeapon < ApplicationRecord
  belongs_to :user
  belongs_to :weapon
  belongs_to :awakening, optional: true

  belongs_to :weapon_key1, class_name: 'WeaponKey', optional: true
  belongs_to :weapon_key2, class_name: 'WeaponKey', optional: true
  belongs_to :weapon_key3, class_name: 'WeaponKey', optional: true
  belongs_to :weapon_key4, class_name: 'WeaponKey', optional: true

  validates :uncap_level, inclusion: { in: 0..5 }
  validates :transcendence_step, inclusion: { in: 0..10 }, allow_nil: true
  validates :awakening_level, inclusion: { in: 1..10 }

  validate :validate_weapon_keys
  validate :validate_ax_skills
  validate :validate_element_change
  validate :validate_awakening_compatibility

  scope :by_weapon, ->(weapon_id) { where(weapon_id: weapon_id) }
  scope :by_series, ->(series) { joins(:weapon).where(weapons: { series: series }) }
  scope :with_keys, -> { where.not(weapon_key1_id: nil) }
  scope :with_ax, -> { where.not(ax_modifier1: nil) }

  def blueprint
    CollectionWeaponBlueprint
  end

  def weapon_keys
    [weapon_key1, weapon_key2, weapon_key3, weapon_key4].compact
  end

  private

  def validate_weapon_keys
    return unless weapon.present?

    weapon_keys.each do |key|
      unless weapon.compatible_with_key?(key)
        errors.add(:weapon_keys, "#{key.name_en} is not compatible with this weapon")
      end
    end

    # Check for duplicate keys
    key_ids = [weapon_key1_id, weapon_key2_id, weapon_key3_id, weapon_key4_id].compact
    if key_ids.length != key_ids.uniq.length
      errors.add(:weapon_keys, "cannot have duplicate keys")
    end
  end

  def validate_ax_skills
    return unless weapon.present? && weapon.ax

    if (ax_modifier1.present? && ax_strength1.blank?) ||
       (ax_modifier1.blank? && ax_strength1.present?)
      errors.add(:ax_modifier1, "AX skill 1 must have both modifier and strength")
    end

    if (ax_modifier2.present? && ax_strength2.blank?) ||
       (ax_modifier2.blank? && ax_strength2.present?)
      errors.add(:ax_modifier2, "AX skill 2 must have both modifier and strength")
    end
  end

  def validate_element_change
    return unless element.present? && weapon.present?

    unless Weapon.element_changeable?(weapon.series)
      errors.add(:element, "cannot be changed for this weapon series")
    end
  end

  def validate_awakening_compatibility
    return unless awakening.present?

    unless awakening.object_type == 'Weapon'
      errors.add(:awakening, "must be a weapon awakening")
    end
  end
end

2.3 Create CollectionSummon model

# app/models/collection_summon.rb
class CollectionSummon < ApplicationRecord
  belongs_to :user
  belongs_to :summon

  validates :uncap_level, inclusion: { in: 0..5 }
  validates :transcendence_step, inclusion: { in: 0..10 }

  scope :by_summon, ->(summon_id) { where(summon_id: summon_id) }
  scope :by_element, ->(element) { joins(:summon).where(summons: { element: element }) }
  scope :transcended, -> { where('transcendence_step > 0') }

  def blueprint
    CollectionSummonBlueprint
  end
end

2.4 Create CollectionJobAccessory model

# app/models/collection_job_accessory.rb
class CollectionJobAccessory < ApplicationRecord
  belongs_to :user
  belongs_to :job_accessory

  validates :job_accessory_id, uniqueness: { scope: :user_id,
    message: "already exists in your collection" }

  scope :by_job, ->(job_id) { joins(:job_accessory).where(job_accessories: { job_id: job_id }) }

  def blueprint
    CollectionJobAccessoryBlueprint
  end
end

2.5 Update User model

# app/models/user.rb - Add these associations and methods

# Associations
has_many :collection_characters, dependent: :destroy
has_many :collection_weapons, dependent: :destroy
has_many :collection_summons, dependent: :destroy
has_many :collection_job_accessories, dependent: :destroy

# Note: The crew association will be added when crews feature is implemented
# belongs_to :crew, optional: true

# Enum for collection privacy levels
enum collection_privacy: {
  public: 0,
  crew_only: 1,
  private: 2
}

# Add collection statistics method
def collection_statistics
  {
    total_characters: collection_characters.count,
    total_weapons: collection_weapons.count,
    total_summons: collection_summons.count,
    total_job_accessories: collection_job_accessories.count,
    unique_weapons: collection_weapons.distinct.count(:weapon_id),
    unique_summons: collection_summons.distinct.count(:summon_id)
  }
end

# Check if collection is viewable by another user
def collection_viewable_by?(viewer)
  return true if self == viewer # Owners can always view their own collection

  case collection_privacy
  when 'public'
    true
  when 'crew_only'
    # Will be implemented when crew feature is added:
    # viewer.present? && crew.present? && viewer.crew_id == crew_id
    false # For now, crew_only acts like private until crews are implemented
  when 'private'
    false
  else
    false
  end
end

# Helper method to check if user is in same crew (placeholder for future)
def in_same_crew_as?(other_user)
  # Will be implemented when crew feature is added:
  # return false unless other_user.present?
  # crew.present? && other_user.crew_id == crew_id
  false
end

Step 3: Create Blueprints

3.1 CollectionCharacterBlueprint

# app/blueprints/api/v1/collection_character_blueprint.rb
module Api
  module V1
    class CollectionCharacterBlueprint < ApiBlueprint
      identifier :id

      fields :uncap_level, :transcendence_step, :perpetuity,
             :ring1, :ring2, :ring3, :ring4, :earring,
             :created_at, :updated_at

      field :awakening, if: ->(_, obj, _) { obj.awakening.present? } do |obj|
        {
          type: AwakeningBlueprint.render_as_hash(obj.awakening),
          level: obj.awakening_level
        }
      end

      association :character, blueprint: CharacterBlueprint, view: :nested

      view :full do
        association :character, blueprint: CharacterBlueprint, view: :full
      end
    end
  end
end

3.2 CollectionWeaponBlueprint

# app/blueprints/api/v1/collection_weapon_blueprint.rb
module Api
  module V1
    class CollectionWeaponBlueprint < ApiBlueprint
      identifier :id

      fields :uncap_level, :transcendence_step, :element,
             :created_at, :updated_at

      field :ax, if: ->(_, obj, _) { obj.ax_modifier1.present? } do |obj|
        [
          { modifier: obj.ax_modifier1, strength: obj.ax_strength1 },
          { modifier: obj.ax_modifier2, strength: obj.ax_strength2 }
        ].compact_blank
      end

      field :awakening, if: ->(_, obj, _) { obj.awakening.present? } do |obj|
        {
          type: AwakeningBlueprint.render_as_hash(obj.awakening),
          level: obj.awakening_level
        }
      end

      association :weapon, blueprint: WeaponBlueprint, view: :nested
      association :weapon_keys, blueprint: WeaponKeyBlueprint,
                  if: ->(_, obj, _) { obj.weapon_keys.any? }

      view :full do
        association :weapon, blueprint: WeaponBlueprint, view: :full
      end
    end
  end
end

3.3 CollectionSummonBlueprint

# app/blueprints/api/v1/collection_summon_blueprint.rb
module Api
  module V1
    class CollectionSummonBlueprint < ApiBlueprint
      identifier :id

      fields :uncap_level, :transcendence_step,
             :created_at, :updated_at

      association :summon, blueprint: SummonBlueprint, view: :nested

      view :full do
        association :summon, blueprint: SummonBlueprint, view: :full
      end
    end
  end
end

3.4 CollectionJobAccessoryBlueprint

# app/blueprints/api/v1/collection_job_accessory_blueprint.rb
module Api
  module V1
    class CollectionJobAccessoryBlueprint < ApiBlueprint
      identifier :id

      fields :created_at, :updated_at

      association :job_accessory, blueprint: JobAccessoryBlueprint
    end
  end
end

Step 4: Create Controllers

4.1 Base Collection Controller (with User Collection Viewing and Privacy)

# app/controllers/api/v1/collection_controller.rb
module Api
  module V1
    class CollectionController < ApiController
      before_action :set_user
      before_action :check_collection_access

      # GET /api/v1/users/:user_id/collection
      # GET /api/v1/users/:user_id/collection?type=weapons
      def show
        collection = case params[:type]
        when 'characters'
          {
            characters: CollectionCharacterBlueprint.render_as_hash(
              @user.collection_characters.includes(:character, :awakening),
              view: :full
            )
          }
        when 'weapons'
          {
            weapons: CollectionWeaponBlueprint.render_as_hash(
              @user.collection_weapons.includes(:weapon, :awakening, :weapon_key1,
                                               :weapon_key2, :weapon_key3, :weapon_key4),
              view: :full
            )
          }
        when 'summons'
          {
            summons: CollectionSummonBlueprint.render_as_hash(
              @user.collection_summons.includes(:summon),
              view: :full
            )
          }
        when 'job_accessories'
          {
            job_accessories: CollectionJobAccessoryBlueprint.render_as_hash(
              @user.collection_job_accessories.includes(job_accessory: :job)
            )
          }
        else
          # Return complete collection
          {
            characters: CollectionCharacterBlueprint.render_as_hash(
              @user.collection_characters.includes(:character, :awakening),
              view: :full
            ),
            weapons: CollectionWeaponBlueprint.render_as_hash(
              @user.collection_weapons.includes(:weapon, :awakening, :weapon_key1,
                                               :weapon_key2, :weapon_key3, :weapon_key4),
              view: :full
            ),
            summons: CollectionSummonBlueprint.render_as_hash(
              @user.collection_summons.includes(:summon),
              view: :full
            ),
            job_accessories: CollectionJobAccessoryBlueprint.render_as_hash(
              @user.collection_job_accessories.includes(job_accessory: :job)
            )
          }
        end

        render json: collection
      end

      def statistics
        stats = @user.collection_statistics
        render json: stats
      end

      private

      def set_user
        @user = User.find(params[:user_id])
      rescue ActiveRecord::RecordNotFound
        render json: { error: "User not found" }, status: :not_found
      end

      def check_collection_access
        unless @user.collection_viewable_by?(current_user)
          render json: { error: "You do not have permission to view this collection" }, status: :forbidden
        end
      end
    end
  end
end

4.2 CollectionCharactersController

# app/controllers/api/v1/collection_characters_controller.rb
module Api
  module V1
    class CollectionCharactersController < ApiController
      before_action :authenticate_user!
      before_action :set_collection_character, only: [:show, :update, :destroy]

      def index
        @collection_characters = current_user.collection_characters
                                             .includes(:character, :awakening)
                                             .page(params[:page])
                                             .per(params[:limit] || 50)

        render json: CollectionCharacterBlueprint.render(
          @collection_characters,
          root: :collection_characters,
          meta: pagination_meta(@collection_characters)
        )
      end

      def show
        render json: CollectionCharacterBlueprint.render(
          @collection_character,
          view: :full
        )
      end

      def create
        @collection_character = current_user.collection_characters.build(collection_character_params)

        if @collection_character.save
          render json: CollectionCharacterBlueprint.render(
            @collection_character,
            view: :full
          ), status: :created
        else
          render_errors(@collection_character.errors)
        end
      end

      def update
        if @collection_character.update(collection_character_params)
          render json: CollectionCharacterBlueprint.render(
            @collection_character,
            view: :full
          )
        else
          render_errors(@collection_character.errors)
        end
      end

      def destroy
        @collection_character.destroy
        head :no_content
      end

      private

      def set_collection_character
        @collection_character = current_user.collection_characters.find(params[:id])
      rescue ActiveRecord::RecordNotFound
        render json: { error: "Collection character not found" }, status: :not_found
      end

      def collection_character_params
        params.require(:collection_character).permit(
          :character_id, :uncap_level, :transcendence_step, :perpetuity,
          :awakening_id, :awakening_level,
          ring1: [:modifier, :strength],
          ring2: [:modifier, :strength],
          ring3: [:modifier, :strength],
          ring4: [:modifier, :strength],
          earring: [:modifier, :strength]
        )
      end
    end
  end
end

4.3 CollectionWeaponsController

# app/controllers/api/v1/collection_weapons_controller.rb
module Api
  module V1
    class CollectionWeaponsController < ApiController
      before_action :authenticate_user!
      before_action :set_collection_weapon, only: [:show, :update, :destroy]

      def index
        @collection_weapons = current_user.collection_weapons
                                          .includes(:weapon, :awakening,
                                                   :weapon_key1, :weapon_key2,
                                                   :weapon_key3, :weapon_key4)

        @collection_weapons = @collection_weapons.by_weapon(params[:weapon_id]) if params[:weapon_id]

        @collection_weapons = @collection_weapons.page(params[:page]).per(params[:limit] || 50)

        render json: CollectionWeaponBlueprint.render(
          @collection_weapons,
          root: :collection_weapons,
          meta: pagination_meta(@collection_weapons)
        )
      end

      def show
        render json: CollectionWeaponBlueprint.render(
          @collection_weapon,
          view: :full
        )
      end

      def create
        @collection_weapon = current_user.collection_weapons.build(collection_weapon_params)

        if @collection_weapon.save
          render json: CollectionWeaponBlueprint.render(
            @collection_weapon,
            view: :full
          ), status: :created
        else
          render_errors(@collection_weapon.errors)
        end
      end

      def update
        if @collection_weapon.update(collection_weapon_params)
          render json: CollectionWeaponBlueprint.render(
            @collection_weapon,
            view: :full
          )
        else
          render_errors(@collection_weapon.errors)
        end
      end

      def destroy
        @collection_weapon.destroy
        head :no_content
      end

      private

      def set_collection_weapon
        @collection_weapon = current_user.collection_weapons.find(params[:id])
      rescue ActiveRecord::RecordNotFound
        render json: { error: "Collection weapon not found" }, status: :not_found
      end

      def collection_weapon_params
        params.require(:collection_weapon).permit(
          :weapon_id, :uncap_level, :transcendence_step,
          :weapon_key1_id, :weapon_key2_id, :weapon_key3_id, :weapon_key4_id,
          :awakening_id, :awakening_level,
          :ax_modifier1, :ax_strength1, :ax_modifier2, :ax_strength2,
          :element
        )
      end
    end
  end
end

4.4 CollectionSummonsController

# app/controllers/api/v1/collection_summons_controller.rb
module Api
  module V1
    class CollectionSummonsController < ApiController
      before_action :authenticate_user!
      before_action :set_collection_summon, only: [:show, :update, :destroy]

      def index
        @collection_summons = current_user.collection_summons
                                          .includes(:summon)

        @collection_summons = @collection_summons.by_summon(params[:summon_id]) if params[:summon_id]

        @collection_summons = @collection_summons.page(params[:page]).per(params[:limit] || 50)

        render json: CollectionSummonBlueprint.render(
          @collection_summons,
          root: :collection_summons,
          meta: pagination_meta(@collection_summons)
        )
      end

      def show
        render json: CollectionSummonBlueprint.render(
          @collection_summon,
          view: :full
        )
      end

      def create
        @collection_summon = current_user.collection_summons.build(collection_summon_params)

        if @collection_summon.save
          render json: CollectionSummonBlueprint.render(
            @collection_summon,
            view: :full
          ), status: :created
        else
          render_errors(@collection_summon.errors)
        end
      end

      def update
        if @collection_summon.update(collection_summon_params)
          render json: CollectionSummonBlueprint.render(
            @collection_summon,
            view: :full
          )
        else
          render_errors(@collection_summon.errors)
        end
      end

      def destroy
        @collection_summon.destroy
        head :no_content
      end

      private

      def set_collection_summon
        @collection_summon = current_user.collection_summons.find(params[:id])
      rescue ActiveRecord::RecordNotFound
        render json: { error: "Collection summon not found" }, status: :not_found
      end

      def collection_summon_params
        params.require(:collection_summon).permit(
          :summon_id, :uncap_level, :transcendence_step
        )
      end
    end
  end
end

4.5 CollectionJobAccessoriesController

# app/controllers/api/v1/collection_job_accessories_controller.rb
module Api
  module V1
    class CollectionJobAccessoriesController < ApiController
      before_action :authenticate_user!
      before_action :set_collection_job_accessory, only: [:destroy]

      def index
        @collection_accessories = current_user.collection_job_accessories
                                              .includes(job_accessory: :job)

        if params[:job_id]
          @collection_accessories = @collection_accessories.by_job(params[:job_id])
        end

        @collection_accessories = @collection_accessories.page(params[:page])
                                                         .per(params[:limit] || 50)

        render json: CollectionJobAccessoryBlueprint.render(
          @collection_accessories,
          root: :collection_job_accessories,
          meta: pagination_meta(@collection_accessories)
        )
      end

      def create
        @collection_accessory = current_user.collection_job_accessories
                                           .build(collection_job_accessory_params)

        if @collection_accessory.save
          render json: CollectionJobAccessoryBlueprint.render(
            @collection_accessory
          ), status: :created
        else
          render_errors(@collection_accessory.errors)
        end
      end

      def destroy
        @collection_job_accessory.destroy
        head :no_content
      end

      private

      def set_collection_job_accessory
        @collection_job_accessory = current_user.collection_job_accessories.find(params[:id])
      rescue ActiveRecord::RecordNotFound
        render json: { error: "Collection job accessory not found" }, status: :not_found
      end

      def collection_job_accessory_params
        params.require(:collection_job_accessory).permit(:job_accessory_id)
      end
    end
  end
end

Step 5: Update Routes

# config/routes.rb - Add these routes within the API scope

# User collection viewing (respects privacy settings)
get 'users/:user_id/collection', to: 'collection#show'
get 'users/:user_id/collection/statistics', to: 'collection#statistics'

# Collection management for current user
namespace :collection do
  resources :characters, controller: '/api/v1/collection_characters'
  resources :weapons, controller: '/api/v1/collection_weapons'
  resources :summons, controller: '/api/v1/collection_summons'
  resources :job_accessories, controller: '/api/v1/collection_job_accessories',
            only: [:index, :create, :destroy]
end

Step 6: Add Helper Methods to ApiController

# app/controllers/api/v1/api_controller.rb - Add these helper methods

protected

def pagination_meta(collection)
  {
    current_page: collection.current_page,
    total_pages: collection.total_pages,
    total_count: collection.total_count,
    per_page: collection.limit_value
  }
end

def render_errors(errors, status = :unprocessable_entity)
  render json: { errors: errors.full_messages }, status: status
end

Testing the Implementation

Manual Testing Steps

  1. Start Rails server

    rails server
    
  2. View a user's complete collection

    # Get complete collection
    curl -X GET http://localhost:3000/api/v1/users/USER_ID/collection
    
    # Get only weapons
    curl -X GET http://localhost:3000/api/v1/users/USER_ID/collection?type=weapons
    
    # Get only characters
    curl -X GET http://localhost:3000/api/v1/users/USER_ID/collection?type=characters
    
    # Get only summons
    curl -X GET http://localhost:3000/api/v1/users/USER_ID/collection?type=summons
    
    # Get only job accessories
    curl -X GET http://localhost:3000/api/v1/users/USER_ID/collection?type=job_accessories
    
  3. Get collection statistics

    curl -X GET http://localhost:3000/api/v1/users/USER_ID/collection/statistics
    
  4. Create collection items (authenticated)

    # Create a character
    curl -X POST http://localhost:3000/api/v1/collection/characters \
      -H "Authorization: Bearer YOUR_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"collection_character": {"character_id": "uuid", "uncap_level": 3}}'
    
    # Create a weapon
    curl -X POST http://localhost:3000/api/v1/collection/weapons \
      -H "Authorization: Bearer YOUR_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"collection_weapon": {"weapon_id": "uuid", "uncap_level": 4}}'
    

Deployment Checklist

  • Run all migrations in staging
  • Test all endpoints in staging
  • Verify database indexes are created
  • Test with large datasets
  • Set up error tracking (Sentry/Rollbar)
  • Create backup before deployment
  • Prepare rollback plan
  • Update API documentation
  • Notify frontend team of new endpoints
  • Schedule deployment during low-traffic window
  • Monitor application after deployment

API Endpoint Summary

Public Collection Viewing (Respects Privacy Settings)

  • GET /api/v1/users/:user_id/collection - View complete collection (if not private)
  • GET /api/v1/users/:user_id/collection?type=characters - View characters only (if not private)
  • GET /api/v1/users/:user_id/collection?type=weapons - View weapons only (if not private)
  • GET /api/v1/users/:user_id/collection?type=summons - View summons only (if not private)
  • GET /api/v1/users/:user_id/collection?type=job_accessories - View job accessories only (if not private)
  • GET /api/v1/users/:user_id/collection/statistics - View collection statistics (if not private)

Collection Management (Authentication Required)

  • GET/POST/PUT/DELETE /api/v1/collection/characters - Manage character collection
  • GET/POST/PUT/DELETE /api/v1/collection/weapons - Manage weapon collection
  • GET/POST/PUT/DELETE /api/v1/collection/summons - Manage summon collection
  • GET/POST/DELETE /api/v1/collection/job_accessories - Manage job accessory collection

Privacy Settings (Authentication Required)

To update collection privacy settings, use the existing user update endpoint:

  • PUT /api/v1/users/:id - Update user settings including collection_privacy field

Privacy levels:

  • 0 or "public": Collection is viewable by everyone
  • 1 or "crew_only": Collection is viewable only by crew members (when crew feature is implemented)
  • 2 or "private": Collection is viewable only by the owner

Example request:

{
  "user": {
    "collection_privacy": "crew_only"
  }
}