Skip to content

Allow multiple digital stores per game #91

@mforce

Description

@mforce

Problem

A game can currently only be associated with a single digital store (DigitalStore? — one value from Steam, GOG, Epic, Xbox, PSN, Nintendo, Other). In practice many games are owned on multiple stores (e.g. Steam + Epic giveaway, or Steam + GOG). Users have to pick one or create duplicate entries.

Current model

// Domain entity
public bool IsDigital { get; set; }
public DigitalStore? DigitalStore { get; set; }  // single value

// Enum
public enum DigitalStore
{
    Steam = 0, Gog = 1, Epic = 2, Xbox = 3,
    Psn = 4, Nintendo = 5, Other = 99,
}

Frontend: checkbox "Digital copy" + single <Select> dropdown for store (GameForm.tsx:202).

Proposed solution

Replace the single-value DigitalStore? with a collection of stores (flags enum or join table).

Approach A — Flags enum (recommended for MVP)

Change DigitalStore to a [Flags] enum and store as a bitmask on the Game entity:

[Flags]
public enum DigitalStore
{
    None    = 0,
    Steam   = 1,
    Gog     = 2,
    Epic    = 4,
    Xbox    = 8,
    Psn     = 16,
    Nintendo = 32,
    Other   = 64,
}

Entity change:

// Remove IsDigital — derive from DigitalStore != None
public DigitalStore DigitalStores { get; set; } = DigitalStore.None;

Remove IsDigital property; a game is digital when DigitalStores != DigitalStore.None. Physical + digital ownership is already modeled separately (the Platform field covers the physical side).

Pros: Zero schema migration complexity (single int column), no new entity, consistent with how MovieFormat already works in this codebase.

Cons: Limited to ~7 predefined stores before running into bit-position constraints (currently 7 + None = 8 values, fits in a byte). Adding new stores requires an enum update + EF migration if values shift.

Approach B — Join table

New entity GameDigitalStore with GameId + DigitalStore columns. More flexible for future expansion (free-text store names, per-store acquisition dates/prices) but heavier lift.

Recommendation: Start with Approach A (flags enum). If per-store metadata is needed later, migrate to Approach B.

Changes

Backend

File Change
Domain/Enums/DigitalStore.cs Add [Flags], renumber as powers of 2, add None = 0
Domain/Entities/Game.cs Replace IsDigital (bool) + DigitalStore? (enum?) with DigitalStores (flags enum, default None)
Infrastructure/Data/CollectifyDbContext.cs Update configuration if any
Api/Endpoints/GasesEndpoints.cs Update DTO, filters, ApplyDto, ToDto — accept DigitalStores as bitmask int. Derive "is digital" from DigitalStores != None for filtering.
Migration New EF migration for column rename + backfill

Frontend

File Change
services/types.ts Replace isDigital: boolean + digitalStore?: DigitalStore with digitalStores: number (bitmask). Add DIGITAL_STORE_FLAGS array mirroring MOVIE_FORMAT_FLAGS pattern.
components/GameForm.tsx Replace checkbox + single select with toggle buttons (same UX as MOVIE_FORMAT_FLAGS in MovieForm.tsx)
components/DetailView.tsx Show multiple store pills instead of single "Digital (Steam)" pill
pages/GamesList.tsx Show store icon(s)/label(s) in tertiary line
components/FiltersPanel.tsx Update digital store filter to multi-select checkboxes

Data migration

Backfill existing rows: if IsDigital == true and DigitalStore is set, copy to corresponding bit in DigitalStores. If IsDigital == true but DigitalStore is null, set DigitalStores = Other.

Out of scope

Verification

  • dotnet build clean, dotnet test passes
  • Add a game with Steam + Epic selected → both stored and displayed
  • Edit an existing physical-only game, add digital stores → platform preserved
  • Filter games by digital store → correct results
  • Existing rows survive migration with stores preserved

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions